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 +

+
+ +
+
+
+ + +
+
+ + + +
+
+ +
+ +
+ +
+

+ Admin access required. Default: admin / admin123 +

+
+
+
+
+ ) +} + +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 */} +
+ + + + + + + + + + + + + + {tenants.map((tenant) => ( + + + + + + + + + + ))} + +
+ Tenant + + Domain + + Auth Provider + + Subscription + + Users + + Created + + Actions +
+
+
+
+ +
+
+
+
{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" + /> +
+
+ +
+ + + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + + ))} + +
+ User + + Role + + Status + + Last Login + + Created +
+
+
+
+ + {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