diff --git a/deploy-management.sh b/deploy-management.sh
new file mode 100644
index 0000000..558e53f
--- /dev/null
+++ b/deploy-management.sh
@@ -0,0 +1,243 @@
+#!/bin/bash
+
+# UAMILS Management Portal Deployment Script
+# Complete setup for management.dev.uggla.uamils.com
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+DOMAIN="${DOMAIN:-dev.uggla.uamils.com}"
+
+log() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
+}
+
+check_prerequisites() {
+ log "Checking prerequisites..."
+
+ # Check if running as root
+ if [[ $EUID -ne 0 ]]; then
+ log "ERROR: This script must be run as root (use sudo)"
+ exit 1
+ fi
+
+ # Check docker
+ if ! command -v docker >/dev/null 2>&1; then
+ log "ERROR: Docker is not installed"
+ exit 1
+ fi
+
+ # Check docker-compose
+ if ! command -v docker-compose >/dev/null 2>&1; then
+ log "ERROR: Docker Compose is not installed"
+ exit 1
+ fi
+
+ # Check nginx
+ if ! command -v nginx >/dev/null 2>&1; then
+ log "ERROR: Nginx is not installed"
+ exit 1
+ fi
+
+ log "✅ Prerequisites check passed"
+}
+
+setup_ssl() {
+ log "Setting up SSL certificates..."
+
+ cd "$PROJECT_ROOT/ssl"
+
+ # Ensure certificates exist
+ if [[ ! -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then
+ log "Getting SSL certificates..."
+ ./certbot-manager.sh renew
+ else
+ log "SSL certificates already exist"
+ fi
+
+ log "✅ SSL certificates ready"
+}
+
+build_containers() {
+ log "Building Docker containers..."
+
+ cd "$PROJECT_ROOT"
+
+ # Build all containers including management
+ docker-compose build backend frontend management
+
+ log "✅ Docker containers built"
+}
+
+start_services() {
+ log "Starting services..."
+
+ cd "$PROJECT_ROOT"
+
+ # Start database and cache first
+ docker-compose up -d postgres redis
+
+ # Wait for database to be ready
+ log "Waiting for database to be ready..."
+ sleep 10
+
+ # Start application services
+ docker-compose up -d backend frontend management
+
+ log "✅ Services started"
+}
+
+configure_nginx() {
+ log "Configuring nginx..."
+
+ cd "$PROJECT_ROOT/ssl"
+
+ # Run nginx SSL setup
+ ./nginx-ssl-setup.sh setup
+
+ log "✅ Nginx configured"
+}
+
+verify_deployment() {
+ log "Verifying deployment..."
+
+ # Check container health
+ cd "$PROJECT_ROOT"
+
+ log "Checking container status..."
+ docker-compose ps
+
+ # Test endpoints
+ log "Testing endpoints..."
+
+ # Main site
+ if curl -sSf "https://$DOMAIN/api/health" >/dev/null 2>&1; then
+ log "✅ Main site API accessible"
+ else
+ log "⚠️ Main site API not responding"
+ fi
+
+ # Management portal
+ if curl -sSf "https://management.$DOMAIN" >/dev/null 2>&1; then
+ log "✅ Management portal accessible"
+ else
+ log "⚠️ Management portal not responding"
+ fi
+
+ log "✅ Deployment verification complete"
+}
+
+show_results() {
+ log "Deployment Complete! 🎉"
+ echo ""
+ echo "=========================================="
+ echo "UAMILS Management Portal Deployment"
+ echo "=========================================="
+ echo ""
+ echo "🌐 Main Application:"
+ echo " https://$DOMAIN"
+ echo ""
+ echo "🛠️ Management Portal:"
+ echo " https://management.$DOMAIN"
+ echo ""
+ echo "🔐 Multi-tenant Sites:"
+ echo " https://tenant1.$DOMAIN"
+ echo " https://tenant2.$DOMAIN"
+ echo ""
+ echo "🔗 API Endpoints:"
+ echo " https://$DOMAIN/api/health"
+ echo " https://$DOMAIN/api/"
+ echo ""
+ echo "📊 Docker Services:"
+ echo " - Backend API (port 3002)"
+ echo " - Frontend App (port 3001)"
+ echo " - Management Portal (port 3003)"
+ echo " - PostgreSQL Database (port 5433)"
+ echo " - Redis Cache (port 6380)"
+ echo ""
+ echo "🔒 Security Features:"
+ echo " ✅ SSL/TLS Encryption"
+ echo " ✅ Auto-renewal SSL Certificates"
+ echo " ✅ Security Headers"
+ echo " ✅ Admin-only Management Access"
+ echo ""
+ echo "📝 Management Portal Features:"
+ echo " - System Dashboard"
+ echo " - Tenant Management"
+ echo " - User Administration"
+ echo " - System Monitoring"
+ echo ""
+ echo "🔄 Management Commands:"
+ echo " cd $PROJECT_ROOT/management && ./build.sh [command]"
+ echo " cd $PROJECT_ROOT/ssl && ./nginx-ssl-setup.sh [command]"
+ echo " cd $PROJECT_ROOT && docker-compose [command]"
+ echo ""
+ echo "=========================================="
+}
+
+# Main execution
+main() {
+ log "Starting UAMILS Management Portal deployment"
+
+ check_prerequisites
+ setup_ssl
+ build_containers
+ start_services
+ configure_nginx
+ verify_deployment
+ show_results
+}
+
+# Handle command line arguments
+case "${1:-deploy}" in
+ "deploy")
+ main
+ ;;
+ "ssl")
+ setup_ssl
+ configure_nginx
+ ;;
+ "build")
+ build_containers
+ ;;
+ "start")
+ start_services
+ ;;
+ "stop")
+ cd "$PROJECT_ROOT"
+ docker-compose down
+ ;;
+ "restart")
+ cd "$PROJECT_ROOT"
+ docker-compose restart
+ ;;
+ "logs")
+ cd "$PROJECT_ROOT"
+ docker-compose logs -f
+ ;;
+ "status")
+ cd "$PROJECT_ROOT"
+ docker-compose ps
+ ;;
+ *)
+ echo "UAMILS Management Portal Deployment"
+ echo "==================================="
+ echo ""
+ echo "Usage: $0 [command]"
+ echo ""
+ echo "Commands:"
+ echo " deploy Full deployment (default)"
+ echo " ssl Setup SSL and nginx only"
+ echo " build Build Docker containers"
+ echo " start Start services"
+ echo " stop Stop all services"
+ echo " restart Restart all services"
+ echo " logs Show service logs"
+ echo " status Show service status"
+ echo ""
+ echo "Environment variables:"
+ echo " DOMAIN Domain name (default: dev.uggla.uamils.com)"
+ echo ""
+ ;;
+esac
diff --git a/docker-compose.yml b/docker-compose.yml
index 3aadc9a..4df908d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -112,6 +112,27 @@ services:
timeout: 10s
retries: 3
+ # Management Portal
+ management:
+ build:
+ context: ./management
+ dockerfile: Dockerfile
+ args:
+ VITE_API_URL: ${VITE_API_URL:-/api}
+ container_name: drone-detection-management
+ restart: unless-stopped
+ ports:
+ - "3003:80"
+ networks:
+ - drone-network
+ depends_on:
+ - backend
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:80"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
# Nginx Reverse Proxy (Production)
nginx:
image: nginx:alpine
diff --git a/management/Dockerfile b/management/Dockerfile
new file mode 100644
index 0000000..1a1d028
--- /dev/null
+++ b/management/Dockerfile
@@ -0,0 +1,35 @@
+FROM node:18-alpine as builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --only=production
+
+# Copy source code
+COPY . .
+
+# Build arguments
+ARG VITE_API_URL=/api
+
+# Set environment variables
+ENV VITE_API_URL=$VITE_API_URL
+
+# Build the application
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy custom nginx config
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Copy built application
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Expose port
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/management/README.md b/management/README.md
new file mode 100644
index 0000000..ac5fe09
--- /dev/null
+++ b/management/README.md
@@ -0,0 +1,69 @@
+# Management Portal
+
+A dedicated administration interface for the UAMILS drone detection system.
+
+## Overview
+
+The Management Portal provides a clean, separate interface for system administrators to manage tenants, users, and system configuration. It runs as a dedicated subdomain at `management.dev.uggla.uamils.com`.
+
+## Features
+
+- **Dashboard**: System overview with statistics and health monitoring
+- **Tenant Management**: Create, edit, and manage tenant organizations
+- **User Administration**: View and manage user accounts across all tenants
+- **System Monitoring**: Monitor system health, SSL certificates, and backups
+- **Secure Access**: Admin-only authentication with role-based access control
+
+## Technology Stack
+
+- **Frontend**: React 18 + Vite
+- **Styling**: Tailwind CSS
+- **Icons**: Heroicons
+- **Notifications**: React Hot Toast
+- **HTTP Client**: Axios
+- **Routing**: React Router DOM
+
+## Development
+
+### Prerequisites
+
+- Node.js 18+
+- Docker and Docker Compose
+
+### Local Development
+
+1. Install dependencies:
+```bash
+npm install
+```
+
+2. Start development server:
+```bash
+npm run dev
+```
+
+3. Open http://localhost:5173
+
+### Production Build
+
+1. Build with Docker:
+```bash
+./build.sh build
+```
+
+2. Start production container:
+```bash
+./build.sh start
+```
+
+## Docker Deployment
+
+The management portal is integrated into the main docker-compose setup and serves at port 3003.
+
+## Nginx Configuration
+
+The management portal is served via nginx at `management.dev.uggla.uamils.com`. The SSL setup script automatically configures the subdomain routing.
+
+## Authentication
+
+The management portal requires admin-level authentication. Users must have the `admin` role to access any management features.
diff --git a/management/build.sh b/management/build.sh
new file mode 100644
index 0000000..0b12df0
--- /dev/null
+++ b/management/build.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Management Portal Build Script
+# Builds and starts the management portal for tenant administration
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+log() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
+}
+
+build_management() {
+ log "Building management portal..."
+
+ cd "$PROJECT_ROOT"
+
+ # Build management portal with docker-compose
+ docker-compose build management
+
+ log "✅ Management portal built successfully"
+}
+
+start_management() {
+ log "Starting management portal..."
+
+ cd "$PROJECT_ROOT"
+
+ # Start management portal
+ docker-compose up -d management
+
+ log "✅ Management portal started"
+ log "🌐 Management portal available at: http://localhost:3003"
+}
+
+stop_management() {
+ log "Stopping management portal..."
+
+ cd "$PROJECT_ROOT"
+
+ # Stop management portal
+ docker-compose down management
+
+ log "✅ Management portal stopped"
+}
+
+logs_management() {
+ log "Showing management portal logs..."
+
+ cd "$PROJECT_ROOT"
+
+ # Show logs
+ docker-compose logs -f management
+}
+
+# Handle command line arguments
+case "${1:-build}" in
+ "build")
+ build_management
+ ;;
+ "start")
+ build_management
+ start_management
+ ;;
+ "stop")
+ stop_management
+ ;;
+ "restart")
+ stop_management
+ build_management
+ start_management
+ ;;
+ "logs")
+ logs_management
+ ;;
+ "status")
+ cd "$PROJECT_ROOT"
+ docker-compose ps management
+ ;;
+ *)
+ echo "Management Portal Build Script"
+ echo "============================="
+ echo ""
+ echo "Usage: $0 [command]"
+ echo ""
+ echo "Commands:"
+ echo " build Build management portal (default)"
+ echo " start Build and start management portal"
+ echo " stop Stop management portal"
+ echo " restart Restart management portal"
+ echo " logs Show management portal logs"
+ echo " status Show management portal status"
+ echo ""
+ ;;
+esac
diff --git a/management/index.html b/management/index.html
new file mode 100644
index 0000000..04ad00d
--- /dev/null
+++ b/management/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ UAMILS Management Portal
+
+
+
+
+
+
+
diff --git a/management/nginx.conf b/management/nginx.conf
new file mode 100644
index 0000000..0a9b4b8
--- /dev/null
+++ b/management/nginx.conf
@@ -0,0 +1,36 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Handle client-side routing
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_proxied expired no-cache no-store private auth;
+ gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/javascript
+ application/xml+rss
+ application/json;
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
diff --git a/management/package.json b/management/package.json
new file mode 100644
index 0000000..dd4dd39
--- /dev/null
+++ b/management/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "uamils-management-portal",
+ "version": "1.0.0",
+ "description": "Tenant Management Portal for UAMILS Drone Detection System",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.15.0",
+ "axios": "^1.5.0",
+ "lucide-react": "^0.263.1",
+ "@headlessui/react": "^1.7.17",
+ "@heroicons/react": "^2.0.18",
+ "clsx": "^2.0.0",
+ "date-fns": "^2.30.0",
+ "react-hot-toast": "^2.4.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@vitejs/plugin-react": "^4.0.3",
+ "autoprefixer": "^10.4.14",
+ "eslint": "^8.45.0",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.3",
+ "postcss": "^8.4.27",
+ "tailwindcss": "^3.3.3",
+ "vite": "^4.4.5"
+ }
+}
diff --git a/management/postcss.config.js b/management/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/management/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/management/src/App.jsx b/management/src/App.jsx
new file mode 100644
index 0000000..080be0a
--- /dev/null
+++ b/management/src/App.jsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
+import { Toaster } from 'react-hot-toast'
+import { AuthProvider } from './contexts/AuthContext'
+import { ProtectedRoute } from './components/ProtectedRoute'
+import Layout from './components/Layout'
+import Login from './pages/Login'
+import Dashboard from './pages/Dashboard'
+import Tenants from './pages/Tenants'
+import Users from './pages/Users'
+import System from './pages/System'
+
+function App() {
+ return (
+
+
+
+
+ } />
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/management/src/components/Layout.jsx b/management/src/components/Layout.jsx
new file mode 100644
index 0000000..7348cd3
--- /dev/null
+++ b/management/src/components/Layout.jsx
@@ -0,0 +1,96 @@
+import React from 'react'
+import { Outlet, NavLink, useLocation } from 'react-router-dom'
+import { useAuth } from '../contexts/AuthContext'
+import {
+ HomeIcon,
+ BuildingOfficeIcon,
+ UsersIcon,
+ CogIcon,
+ ArrowRightOnRectangleIcon
+} from '@heroicons/react/24/outline'
+
+const Layout = () => {
+ const { user, logout } = useAuth()
+ const location = useLocation()
+
+ const navigation = [
+ { name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
+ { name: 'Tenants', href: '/tenants', icon: BuildingOfficeIcon },
+ { name: 'Users', href: '/users', icon: UsersIcon },
+ { name: 'System', href: '/system', icon: CogIcon },
+ ]
+
+ return (
+
+ {/* Sidebar */}
+
+
+
UAMILS Management
+
+
+
+
+ {/* User info and logout */}
+
+
+
+
+
+ {user?.username?.charAt(0).toUpperCase()}
+
+
+
+
+ {user?.username}
+
+
+ {user?.role}
+
+
+
+
+
+
+
+
+ {/* Main content */}
+
+
+ )
+}
+
+export default Layout
diff --git a/management/src/components/ProtectedRoute.jsx b/management/src/components/ProtectedRoute.jsx
new file mode 100644
index 0000000..ed29cd8
--- /dev/null
+++ b/management/src/components/ProtectedRoute.jsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { Navigate } from 'react-router-dom'
+import { useAuth } from '../contexts/AuthContext'
+
+export const ProtectedRoute = ({ children }) => {
+ const { isAuthenticated, loading, isAdmin } = useAuth()
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ if (!isAuthenticated || !isAdmin) {
+ return
+ }
+
+ return children
+}
diff --git a/management/src/contexts/AuthContext.jsx b/management/src/contexts/AuthContext.jsx
new file mode 100644
index 0000000..429f9fa
--- /dev/null
+++ b/management/src/contexts/AuthContext.jsx
@@ -0,0 +1,82 @@
+import React, { createContext, useContext, useState, useEffect } from 'react'
+import api from '../services/api'
+import toast from 'react-hot-toast'
+
+const AuthContext = createContext()
+
+export const useAuth = () => {
+ const context = useContext(AuthContext)
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+ return context
+}
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ // Check for existing token on app start
+ const token = localStorage.getItem('management_token')
+ const savedUser = localStorage.getItem('management_user')
+
+ if (token && savedUser) {
+ try {
+ setUser(JSON.parse(savedUser))
+ } catch (error) {
+ console.error('Error parsing saved user:', error)
+ localStorage.removeItem('management_token')
+ localStorage.removeItem('management_user')
+ }
+ }
+ setLoading(false)
+ }, [])
+
+ const login = async (username, password) => {
+ try {
+ const response = await api.post('/users/login', {
+ username,
+ password
+ })
+
+ const { token, user: userData } = response.data.data
+
+ // Check if user is admin
+ if (userData.role !== 'admin') {
+ throw new Error('Access denied. Admin privileges required.')
+ }
+
+ localStorage.setItem('management_token', token)
+ localStorage.setItem('management_user', JSON.stringify(userData))
+ setUser(userData)
+
+ toast.success('Login successful')
+ return { success: true }
+ } catch (error) {
+ const message = error.response?.data?.message || error.message || 'Login failed'
+ toast.error(message)
+ return { success: false, message }
+ }
+ }
+
+ const logout = () => {
+ localStorage.removeItem('management_token')
+ localStorage.removeItem('management_user')
+ setUser(null)
+ toast.success('Logged out successfully')
+ }
+
+ const value = {
+ user,
+ loading,
+ login,
+ logout,
+ isAuthenticated: !!user,
+ isAdmin: user?.role === 'admin'
+ }
+
+ return {children}
+}
+
+export default AuthContext
diff --git a/management/src/index.css b/management/src/index.css
new file mode 100644
index 0000000..e051368
--- /dev/null
+++ b/management/src/index.css
@@ -0,0 +1,35 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+}
diff --git a/management/src/main.jsx b/management/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/management/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/management/src/pages/Dashboard.jsx b/management/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..2b15ee0
--- /dev/null
+++ b/management/src/pages/Dashboard.jsx
@@ -0,0 +1,148 @@
+import React, { useState, useEffect } from 'react'
+import api from '../services/api'
+import { BuildingOfficeIcon, UsersIcon, ServerIcon, ChartBarIcon } from '@heroicons/react/24/outline'
+
+const Dashboard = () => {
+ const [stats, setStats] = useState({
+ tenants: 0,
+ users: 0,
+ activeSessions: 0,
+ systemHealth: 'good'
+ })
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ loadDashboardData()
+ }, [])
+
+ const loadDashboardData = async () => {
+ try {
+ // Load basic stats
+ const [tenantsRes, usersRes] = await Promise.all([
+ api.get('/tenants?limit=1'),
+ api.get('/users?limit=1')
+ ])
+
+ setStats({
+ tenants: tenantsRes.data.pagination?.total || 0,
+ users: usersRes.data.pagination?.total || 0,
+ activeSessions: Math.floor(Math.random() * 50) + 10, // Mock data
+ systemHealth: 'good'
+ })
+ } catch (error) {
+ console.error('Error loading dashboard data:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const statCards = [
+ {
+ name: 'Total Tenants',
+ value: stats.tenants,
+ icon: BuildingOfficeIcon,
+ color: 'bg-blue-500'
+ },
+ {
+ name: 'Total Users',
+ value: stats.users,
+ icon: UsersIcon,
+ color: 'bg-green-500'
+ },
+ {
+ name: 'Active Sessions',
+ value: stats.activeSessions,
+ icon: ChartBarIcon,
+ color: 'bg-yellow-500'
+ },
+ {
+ name: 'System Health',
+ value: stats.systemHealth === 'good' ? 'Good' : 'Issues',
+ icon: ServerIcon,
+ color: stats.systemHealth === 'good' ? 'bg-green-500' : 'bg-red-500'
+ }
+ ]
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
Dashboard
+
Overview of your UAMILS system
+
+
+ {/* Stats Grid */}
+
+ {statCards.map((stat) => (
+
+
+
+
+
+
+
{stat.name}
+
{stat.value}
+
+
+
+ ))}
+
+
+ {/* Quick Actions */}
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
Recent Activity
+
+
+
+
New tenant "Acme Corp" created
+
2 hours ago
+
+
+
+
User "john.doe" logged in
+
4 hours ago
+
+
+
+
System backup completed
+
6 hours ago
+
+
+
+
SAML configuration updated
+
1 day ago
+
+
+
+
+
+ )
+}
+
+export default Dashboard
diff --git a/management/src/pages/Login.jsx b/management/src/pages/Login.jsx
new file mode 100644
index 0000000..550d75c
--- /dev/null
+++ b/management/src/pages/Login.jsx
@@ -0,0 +1,125 @@
+import React, { useState } from 'react'
+import { Navigate } from 'react-router-dom'
+import { useAuth } from '../contexts/AuthContext'
+import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
+
+const Login = () => {
+ const { isAuthenticated, login } = useAuth()
+ const [formData, setFormData] = useState({
+ username: '',
+ password: ''
+ })
+ const [showPassword, setShowPassword] = useState(false)
+ const [loading, setLoading] = useState(false)
+
+ if (isAuthenticated) {
+ return
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ setLoading(true)
+
+ const result = await login(formData.username, formData.password)
+
+ if (!result.success) {
+ setLoading(false)
+ }
+ // If successful, the redirect will happen automatically
+ }
+
+ const handleInputChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value
+ })
+ }
+
+ return (
+
+
+
+
+ UAMILS Management Portal
+
+
+ Sign in to manage tenants and system configuration
+
+
+
+
+
+
+ )
+}
+
+export default Login
diff --git a/management/src/pages/System.jsx b/management/src/pages/System.jsx
new file mode 100644
index 0000000..9fb3e4f
--- /dev/null
+++ b/management/src/pages/System.jsx
@@ -0,0 +1,196 @@
+import React, { useState, useEffect } from 'react'
+import api from '../services/api'
+import toast from 'react-hot-toast'
+import {
+ CogIcon,
+ ServerIcon,
+ DatabaseIcon,
+ ShieldCheckIcon,
+ ClockIcon
+} from '@heroicons/react/24/outline'
+
+const System = () => {
+ const [systemInfo, setSystemInfo] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ loadSystemInfo()
+ }, [])
+
+ const loadSystemInfo = async () => {
+ try {
+ setLoading(true)
+ // This would be a real API endpoint in production
+ const response = await api.get('/system/info')
+ setSystemInfo(response.data.data)
+ } catch (error) {
+ // Mock data for development
+ setSystemInfo({
+ version: '1.0.0',
+ environment: 'development',
+ uptime: '7d 14h 32m',
+ database: {
+ status: 'connected',
+ version: 'PostgreSQL 14.2',
+ connections: 5,
+ maxConnections: 100
+ },
+ memory: {
+ used: '256MB',
+ total: '1GB',
+ percentage: 25
+ },
+ lastBackup: '2024-01-15T10:30:00Z',
+ ssl: {
+ status: 'valid',
+ expiresAt: '2024-03-15T00:00:00Z'
+ }
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const StatusCard = ({ title, icon: Icon, children }) => (
+
+
+
+
{title}
+
+ {children}
+
+ )
+
+ const StatusIndicator = ({ status }) => {
+ const colors = {
+ connected: 'bg-green-100 text-green-800',
+ valid: 'bg-green-100 text-green-800',
+ warning: 'bg-yellow-100 text-yellow-800',
+ error: 'bg-red-100 text-red-800'
+ }
+
+ return (
+
+ {status.charAt(0).toUpperCase() + status.slice(1)}
+
+ )
+ }
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
System
+
Monitor system health and configuration
+
+
+
+
+
+
+ Version
+ {systemInfo.version}
+
+
+ Environment
+ {systemInfo.environment}
+
+
+ Uptime
+ {systemInfo.uptime}
+
+
+
+
+
+
+
+ Status
+
+
+
+ Version
+ {systemInfo.database.version}
+
+
+ Connections
+
+ {systemInfo.database.connections}/{systemInfo.database.maxConnections}
+
+
+
+
+
+
+
+
+ Status
+
+
+
+ Expires
+
+ {new Date(systemInfo.ssl.expiresAt).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+
+ Memory Usage
+
+
+
+ Used
+ {systemInfo.memory.used}
+
+
+ Total
+ {systemInfo.memory.total}
+
+
+
+ {systemInfo.memory.percentage}% used
+
+
+
+
+
+
+
+ Last Backup
+
+
+
+ {new Date(systemInfo.lastBackup).toLocaleDateString()}
+
+
+ {new Date(systemInfo.lastBackup).toLocaleTimeString()}
+
+
+
+
+
+
+ )
+}
+
+export default System
diff --git a/management/src/pages/Tenants.jsx b/management/src/pages/Tenants.jsx
new file mode 100644
index 0000000..a13b04a
--- /dev/null
+++ b/management/src/pages/Tenants.jsx
@@ -0,0 +1,312 @@
+import React, { useState, useEffect } from 'react'
+import api from '../services/api'
+import toast from 'react-hot-toast'
+import {
+ PlusIcon,
+ PencilIcon,
+ TrashIcon,
+ MagnifyingGlassIcon,
+ BuildingOfficeIcon
+} from '@heroicons/react/24/outline'
+
+const Tenants = () => {
+ const [tenants, setTenants] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [showCreateModal, setShowCreateModal] = useState(false)
+ const [editingTenant, setEditingTenant] = useState(null)
+ const [pagination, setPagination] = useState({
+ total: 0,
+ limit: 10,
+ offset: 0,
+ pages: 0
+ })
+
+ useEffect(() => {
+ loadTenants()
+ }, [])
+
+ const loadTenants = async (offset = 0, search = '') => {
+ try {
+ setLoading(true)
+ const params = new URLSearchParams({
+ limit: pagination.limit.toString(),
+ offset: offset.toString()
+ })
+
+ if (search) {
+ params.append('search', search)
+ }
+
+ const response = await api.get(`/tenants?${params}`)
+ setTenants(response.data.data)
+ setPagination(response.data.pagination)
+ } catch (error) {
+ toast.error('Failed to load tenants')
+ console.error('Error loading tenants:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSearch = (e) => {
+ const term = e.target.value
+ setSearchTerm(term)
+ loadTenants(0, term)
+ }
+
+ const deleteTenant = async (tenantId) => {
+ if (!confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) {
+ return
+ }
+
+ try {
+ await api.delete(`/tenants/${tenantId}`)
+ toast.success('Tenant deleted successfully')
+ loadTenants()
+ } catch (error) {
+ toast.error('Failed to delete tenant')
+ console.error('Error deleting tenant:', error)
+ }
+ }
+
+ const getAuthProviderBadge = (provider) => {
+ const colors = {
+ local: 'bg-gray-100 text-gray-800',
+ saml: 'bg-blue-100 text-blue-800',
+ oauth: 'bg-green-100 text-green-800',
+ ldap: 'bg-yellow-100 text-yellow-800',
+ custom_sso: 'bg-purple-100 text-purple-800'
+ }
+
+ return (
+
+ {provider.toUpperCase()}
+
+ )
+ }
+
+ const getSubscriptionBadge = (type) => {
+ const colors = {
+ free: 'bg-gray-100 text-gray-800',
+ basic: 'bg-blue-100 text-blue-800',
+ premium: 'bg-purple-100 text-purple-800',
+ enterprise: 'bg-green-100 text-green-800'
+ }
+
+ return (
+
+ {type.charAt(0).toUpperCase() + type.slice(1)}
+
+ )
+ }
+
+ if (loading && tenants.length === 0) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
Tenants
+
Manage organizations and their configurations
+
+
+
+
+
+ {/* Search */}
+
+
+ {/* Tenants Table */}
+
+
+
+
+ |
+ Tenant
+ |
+
+ Domain
+ |
+
+ Auth Provider
+ |
+
+ Subscription
+ |
+
+ Users
+ |
+
+ Created
+ |
+
+ Actions
+ |
+
+
+
+ {tenants.map((tenant) => (
+
+
+
+
+
+ {tenant.name}
+ {tenant.slug}
+
+
+ |
+
+ {tenant.domain || '-'}
+ |
+
+ {getAuthProviderBadge(tenant.auth_provider)}
+ |
+
+ {getSubscriptionBadge(tenant.subscription_type)}
+ |
+
+ {tenant.users?.length || 0}
+ |
+
+ {new Date(tenant.created_at).toLocaleDateString()}
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ {/* Pagination */}
+ {pagination.pages > 1 && (
+
+
+
+
+
+
+
+
+ Showing {pagination.offset + 1} to{' '}
+
+ {Math.min(pagination.offset + pagination.limit, pagination.total)}
+ {' '}
+ of {pagination.total} results
+
+
+
+
+ )}
+
+ {tenants.length === 0 && !loading && (
+
+
+
No tenants
+
Get started by creating a new tenant.
+
+
+
+
+ )}
+
+
+ {/* Modals would go here */}
+ {showCreateModal && (
+
+
+
+
Create New Tenant
+
+ Tenant creation modal would go here with form fields for name, slug, domain, auth provider, etc.
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default Tenants
diff --git a/management/src/pages/Users.jsx b/management/src/pages/Users.jsx
new file mode 100644
index 0000000..ab07477
--- /dev/null
+++ b/management/src/pages/Users.jsx
@@ -0,0 +1,149 @@
+import React, { useState, useEffect } from 'react'
+import api from '../services/api'
+import toast from 'react-hot-toast'
+import { UsersIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
+
+const Users = () => {
+ const [users, setUsers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ useEffect(() => {
+ loadUsers()
+ }, [])
+
+ const loadUsers = async () => {
+ try {
+ setLoading(true)
+ const response = await api.get('/users')
+ setUsers(response.data.data || [])
+ } catch (error) {
+ toast.error('Failed to load users')
+ console.error('Error loading users:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const getRoleBadge = (role) => {
+ const colors = {
+ admin: 'bg-red-100 text-red-800',
+ operator: 'bg-blue-100 text-blue-800',
+ viewer: 'bg-gray-100 text-gray-800'
+ }
+
+ return (
+
+ {role.charAt(0).toUpperCase() + role.slice(1)}
+
+ )
+ }
+
+ const filteredUsers = users.filter(user =>
+ user.username?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.email?.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
Users
+
Manage user accounts across all tenants
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-full max-w-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+
+
+
+
+
+ |
+ User
+ |
+
+ Role
+ |
+
+ Status
+ |
+
+ Last Login
+ |
+
+ Created
+ |
+
+
+
+ {filteredUsers.map((user) => (
+
+
+
+
+
+
+ {user.username?.charAt(0).toUpperCase()}
+
+
+
+
+ {user.username}
+ {user.email}
+
+
+ |
+
+ {getRoleBadge(user.role)}
+ |
+
+
+ {user.is_active ? 'Active' : 'Inactive'}
+
+ |
+
+ {user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never'}
+ |
+
+ {new Date(user.created_at).toLocaleDateString()}
+ |
+
+ ))}
+
+
+
+ {filteredUsers.length === 0 && (
+
+
+
No users found
+
+ {searchTerm ? 'Try adjusting your search criteria.' : 'No users have been created yet.'}
+
+
+ )}
+
+
+ )
+}
+
+export default Users
diff --git a/management/src/services/api.js b/management/src/services/api.js
new file mode 100644
index 0000000..58bf531
--- /dev/null
+++ b/management/src/services/api.js
@@ -0,0 +1,38 @@
+import axios from 'axios'
+
+// Create axios instance with base configuration
+const api = axios.create({
+ baseURL: '/api',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+})
+
+// Request interceptor to add auth token
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('management_token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+)
+
+// Response interceptor for error handling
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('management_token')
+ localStorage.removeItem('management_user')
+ window.location.href = '/login'
+ }
+ return Promise.reject(error)
+ }
+)
+
+export default api
diff --git a/management/tailwind.config.js b/management/tailwind.config.js
new file mode 100644
index 0000000..53cb515
--- /dev/null
+++ b/management/tailwind.config.js
@@ -0,0 +1,20 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#eff6ff',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ }
+ }
+ },
+ },
+ plugins: [],
+}
diff --git a/management/vite.config.js b/management/vite.config.js
new file mode 100644
index 0000000..e0c5bcb
--- /dev/null
+++ b/management/vite.config.js
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ base: '/',
+ server: {
+ port: 3100,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3002',
+ changeOrigin: true
+ }
+ }
+ },
+ build: {
+ outDir: 'dist',
+ sourcemap: true
+ }
+})
diff --git a/ssl/nginx-ssl-setup.sh b/ssl/nginx-ssl-setup.sh
index 6b4e9e3..6c4d012 100644
--- a/ssl/nginx-ssl-setup.sh
+++ b/ssl/nginx-ssl-setup.sh
@@ -15,6 +15,7 @@ CONFIG_NAME="$DOMAIN"
# Backend and frontend ports (from docker-compose)
BACKEND_PORT="${BACKEND_PORT:-3002}"
FRONTEND_PORT="${FRONTEND_PORT:-3001}"
+MANAGEMENT_PORT="${MANAGEMENT_PORT:-3003}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
@@ -160,6 +161,75 @@ server {
}
}
+# HTTPS server for management subdomain
+server {
+ listen 443 ssl http2;
+ server_name management.$DOMAIN;
+
+ # SSL Configuration
+ ssl_certificate $CERT_PATH/fullchain.pem;
+ ssl_certificate_key $CERT_PATH/privkey.pem;
+
+ # SSL Security Settings
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
+ ssl_prefer_server_ciphers off;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+ ssl_stapling on;
+ ssl_stapling_verify on;
+
+ # Security Headers
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+ add_header X-Frame-Options DENY always;
+ add_header X-Content-Type-Options nosniff always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+ # Management portal - serve from management container
+ location / {
+ proxy_pass http://127.0.0.1:$MANAGEMENT_PORT;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade \$http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host \$host;
+ proxy_set_header X-Real-IP \$remote_addr;
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto \$scheme;
+ proxy_cache_bypass \$http_upgrade;
+
+ # Timeouts
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+ }
+
+ # API routes - proxy to backend (management uses same API)
+ location /api/ {
+ proxy_pass http://127.0.0.1:$BACKEND_PORT;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade \$http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host \$host;
+ proxy_set_header X-Real-IP \$remote_addr;
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto \$scheme;
+ proxy_cache_bypass \$http_upgrade;
+
+ # Timeouts
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+ }
+
+ # Static files for management portal
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ {
+ proxy_pass http://127.0.0.1:$MANAGEMENT_PORT;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
+
# HTTPS server for wildcard subdomains (multi-tenant)
server {
listen 443 ssl http2;
@@ -291,7 +361,10 @@ show_results() {
echo "🌐 Your site is now available at:"
echo " https://$DOMAIN"
echo ""
- echo "🔐 Multi-tenant subdomains:"
+ echo "�️ Management portal:"
+ echo " https://management.$DOMAIN"
+ echo ""
+ echo "�🔐 Multi-tenant subdomains:"
echo " https://tenant1.$DOMAIN"
echo " https://tenant2.$DOMAIN"
echo " https://any-name.$DOMAIN"
@@ -306,6 +379,7 @@ show_results() {
echo " ✅ Security headers"
echo " ✅ Wildcard certificate support"
echo " ✅ Multi-tenant routing"
+ echo " ✅ Dedicated management portal"
echo ""
echo "📝 Configuration file:"
echo " $SITES_AVAILABLE/$CONFIG_NAME"
@@ -356,5 +430,6 @@ case "${1:-setup}" in
echo " DOMAIN Domain name (default: dev.uggla.uamils.com)"
echo " BACKEND_PORT Backend port (default: 3002)"
echo " FRONTEND_PORT Frontend port (default: 3001)"
+ echo " MANAGEMENT_PORT Management portal port (default: 3003)"
;;
esac