Files
boc/landvex-admin-backend/app/routers/system.py
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

499 lines
22 KiB
Python

"""
System Administration Router för LandveX Admin Backend.
Hanterar användare/roller, tenant-hantering, audit log, inställningar,
CloudFront/S3, backup och återställning.
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional, Dict, Any
from datetime import datetime, date
from app.database import get_db
from app.dependencies import get_current_user, require_admin, require_manager
from app.models import User, UserRole
router = APIRouter(prefix="/system", tags=["system"])
# ─── Users & Roles ────────────────────────────────────────────────────────────
@router.get("/users")
async def list_system_users(
role: Optional[str] = Query(None),
status: Optional[str] = Query(None),
tenant_id: Optional[str] = Query(None),
search: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Lista alla systemanvändare med filtrering."""
users = [
{"id": "usr-001", "email": "admin@landvex.com", "first_name": "Admin", "last_name": "User", "role": "admin", "status": "active", "tenant_id": None, "is_superuser": True, "last_login": "2026-07-04T06:00:00Z", "created_at": "2024-01-01T00:00:00Z"},
{"id": "usr-002", "email": "erik@landvex.com", "first_name": "Erik", "last_name": "Svensson", "role": "admin", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-03T18:00:00Z", "created_at": "2024-01-01T00:00:00Z"},
{"id": "usr-003", "email": "johan@landvex.com", "first_name": "Johan", "last_name": "Berglund", "role": "manager", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-04T07:00:00Z", "created_at": "2024-06-01T00:00:00Z"},
{"id": "usr-004", "email": "analyst@landvex.com", "first_name": "Analyst", "last_name": "One", "role": "analyst", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-01T09:00:00Z", "created_at": "2024-03-01T00:00:00Z"},
{"id": "usr-005", "email": "viewer@landvex.com", "first_name": "Viewer", "last_name": "One", "role": "viewer", "status": "inactive", "tenant_id": "tenant-001", "is_superuser": False, "last_login": None, "created_at": "2024-03-01T00:00:00Z"},
]
if role:
users = [u for u in users if u["role"] == role]
if status:
users = [u for u in users if u["status"] == status]
if tenant_id:
users = [u for u in users if u["tenant_id"] == tenant_id]
if search:
users = [u for u in users if search.lower() in u["email"].lower() or search.lower() in u["first_name"].lower() or search.lower() in u["last_name"].lower()]
return {"users": users[offset:offset+limit], "total": len(users), "limit": limit, "offset": offset}
@router.get("/users/{user_id}")
async def get_system_user(
user_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta en specifik användare."""
return {
"id": user_id,
"email": "admin@landvex.com",
"first_name": "Admin",
"last_name": "User",
"role": "admin",
"status": "active",
"tenant_id": None,
"is_superuser": True,
"permissions": ["*"],
"last_login": "2026-07-04T06:00:00Z",
"login_history": [
{"timestamp": "2026-07-04T06:00:00Z", "ip": "192.168.1.1", "user_agent": "Mozilla/5.0"},
{"timestamp": "2026-07-03T18:00:00Z", "ip": "192.168.1.1", "user_agent": "Mozilla/5.0"},
],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2026-07-04T06:00:00Z",
}
@router.patch("/users/{user_id}/role")
async def update_user_role(
user_id: str,
role: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Uppdatera en användares roll."""
return {
"status": "updated",
"user_id": user_id,
"new_role": role,
"updated_by": str(current_user.id),
"updated_at": datetime.now().isoformat(),
}
@router.patch("/users/{user_id}/status")
async def update_user_status(
user_id: str,
status: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Uppdatera en användares status (active/inactive/pending)."""
return {
"status": "updated",
"user_id": user_id,
"new_status": status,
"updated_by": str(current_user.id),
"updated_at": datetime.now().isoformat(),
}
# ─── Roles & Permissions ──────────────────────────────────────────────────────
@router.get("/roles")
async def list_roles(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Lista alla roller och deras behörigheter."""
return {
"roles": [
{
"name": "admin",
"description": "Fullständig åtkomst till alla moduler och inställningar",
"permissions": [
"users:*", "tenants:*", "content:*", "quixzoom:*",
"economy:*", "hr:*", "system:*", "audit:*", "settings:*"
],
"user_count": 2,
},
{
"name": "manager",
"description": "Hantera innehåll, missions och team-medlemmar inom sin tenant",
"permissions": [
"content:read", "content:write", "quixzoom:*",
"economy:read", "hr:read", "hr:write", "audit:read"
],
"user_count": 1,
},
{
"name": "analyst",
"description": "Läsa och analysera data, skapa rapporter",
"permissions": [
"content:read", "quixzoom:read", "economy:read",
"hr:read", "audit:read"
],
"user_count": 1,
},
{
"name": "viewer",
"description": "Endast läsåtkomst till dashboards och rapporter",
"permissions": [
"content:read", "quixzoom:read", "economy:read", "hr:read"
],
"user_count": 1,
},
]
}
# ─── Audit Log ────────────────────────────────────────────────────────────────
@router.get("/audit-log")
async def get_system_audit_log(
action: Optional[str] = Query(None),
entity_type: Optional[str] = Query(None),
user_id: Optional[str] = Query(None),
from_date: Optional[date] = Query(None),
to_date: Optional[date] = Query(None),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_manager),
):
"""Hämta system audit log med filtrering."""
logs = [
{"id": "log-001", "timestamp": "2026-07-04T06:30:00Z", "action": "UPDATE", "entity_type": "article", "entity_id": "decision-first-intelligence", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Updated article content", "success": True},
{"id": "log-002", "timestamp": "2026-07-04T06:15:00Z", "action": "CREATE", "entity_type": "mission", "entity_id": "mission-003", "actor_id": "usr-003", "actor_email": "johan@landvex.com", "actor_ip": "192.168.1.2", "details": "Created mission: Oslo Road Condition", "success": True},
{"id": "log-003", "timestamp": "2026-07-04T06:00:00Z", "action": "LOGIN", "entity_type": "user", "entity_id": "usr-002", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "User login", "success": True},
{"id": "log-004", "timestamp": "2026-07-03T18:00:00Z", "action": "DELETE", "entity_type": "article", "entity_id": "old-draft-001", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Deleted draft article", "success": True},
{"id": "log-005", "timestamp": "2026-07-03T15:30:00Z", "action": "UPDATE", "entity_type": "user", "entity_id": "usr-005", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Changed user status to inactive", "success": True},
{"id": "log-006", "timestamp": "2026-07-03T12:00:00Z", "action": "EXPORT", "entity_type": "report", "entity_id": "payroll-2026-06", "actor_id": "usr-003", "actor_email": "johan@landvex.com", "actor_ip": "192.168.1.2", "details": "Exported payroll report", "success": True},
{"id": "log-007", "timestamp": "2026-07-03T09:00:00Z", "action": "CREATE", "entity_type": "transaction", "entity_id": "tx-001", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Created journal entry", "success": True},
{"id": "log-008", "timestamp": "2026-07-02T16:00:00Z", "action": "UPDATE", "entity_type": "tenant", "entity_id": "tenant-001", "actor_id": "usr-001", "actor_email": "admin@landvex.com", "actor_ip": "10.0.0.1", "details": "Updated tenant settings", "success": True},
{"id": "log-009", "timestamp": "2026-07-02T10:00:00Z", "action": "INVALIDATE", "entity_type": "cloudfront", "entity_id": "E2M3J95HLUR89H", "actor_id": "system", "actor_email": "system", "actor_ip": None, "details": "Auto-invalidated cache", "success": True},
{"id": "log-010", "timestamp": "2026-07-01T08:00:00Z", "action": "BACKUP", "entity_type": "database", "entity_id": "landvex-admin", "actor_id": "system", "actor_email": "system", "actor_ip": None, "details": "Daily backup completed", "success": True},
]
if action:
logs = [l for l in logs if l["action"] == action]
if entity_type:
logs = [l for l in logs if l["entity_type"] == entity_type]
if user_id:
logs = [l for l in logs if l["actor_id"] == user_id]
return {"logs": logs[offset:offset+limit], "total": len(logs), "limit": limit, "offset": offset}
# ─── Settings ─────────────────────────────────────────────────────────────────
@router.get("/settings")
async def get_all_settings(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta alla systeminställningar."""
return {
"general": {
"company_name": "Landvex AB",
"org_number": "559123-4567",
"address": "Storgatan 1, 111 22 Stockholm",
"phone": "+46 8 123 45 67",
"email": "info@landvex.com",
"website": "https://landvex.com",
"currency": "SEK",
"language": "sv",
"timezone": "Europe/Stockholm",
},
"content": {
"auto_publish": False,
"publish_schedule": "manual",
"seo_auto_optimize": True,
"default_author": "Landvex Team",
"featured_image_required": False,
},
"quixzoom": {
"mission_auto_approve": False,
"min_quality_score": 85,
"payment_threshold": 100,
"max_mission_duration_days": 14,
"contributor_onboarding_required": True,
},
"economy": {
"ledger_sync_interval": "hourly",
"default_vat_rate": 25,
"fiscal_year_start": "01-01",
"invoice_prefix": "INV",
"payment_terms_days": 30,
},
"hr": {
"vacation_days_per_year": 25,
"sick_leave_notification_days": 1,
"work_hours_per_day": 8,
"overtime_threshold": 40,
},
"notifications": {
"email_enabled": True,
"slack_enabled": False,
"slack_webhook_url": None,
"alert_on_system_error": True,
"alert_on_low_balance": True,
"low_balance_threshold": 50000,
},
"security": {
"mfa_required": False,
"password_min_length": 8,
"password_expiry_days": 90,
"session_timeout_minutes": 60,
"max_login_attempts": 5,
"lockout_duration_minutes": 30,
},
}
@router.put("/settings/{module}")
async def update_module_settings(
module: str,
settings: Dict[str, Any],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Uppdatera inställningar för en specifik modul."""
valid_modules = ["general", "content", "quixzoom", "economy", "hr", "notifications", "security"]
if module not in valid_modules:
raise HTTPException(status_code=400, detail=f"Invalid module. Must be one of: {', '.join(valid_modules)}")
return {
"status": "updated",
"module": module,
"settings": settings,
"updated_by": str(current_user.id),
"updated_at": datetime.now().isoformat(),
}
# ─── CloudFront / S3 ──────────────────────────────────────────────────────────
@router.get("/cloudfront")
async def get_cloudfront_distributions(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta CloudFront distributions."""
return {
"distributions": [
{
"id": "E2M3J95HLUR89H",
"domain": "d1234.cloudfront.net",
"status": "Deployed",
"enabled": True,
"origin": "landvex-prod.s3.eu-north-1.amazonaws.com",
"ssl_certificate": "landvex.com",
"price_class": "PriceClass_100",
"last_modified": "2026-07-01T00:00:00Z",
"invalidations": {"pending": 0, "completed": 45},
},
{
"id": "EE30B9WM5ZYM7",
"domain": "d5678.cloudfront.net",
"status": "Deployed",
"enabled": True,
"origin": "quixzoom-landing-prod.s3.eu-north-1.amazonaws.com",
"ssl_certificate": "quixzoom.com",
"price_class": "PriceClass_100",
"last_modified": "2026-06-15T00:00:00Z",
"invalidations": {"pending": 0, "completed": 12},
},
]
}
@router.get("/cloudfront/{distribution_id}")
async def get_cloudfront_distribution(
distribution_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta detaljer för en CloudFront distribution."""
return {
"id": distribution_id,
"domain": "d1234.cloudfront.net",
"status": "Deployed",
"enabled": True,
"origin": "landvex-prod.s3.eu-north-1.amazonaws.com",
"ssl_certificate": "landvex.com",
"price_class": "PriceClass_100",
"cache_behaviors": [
{"path_pattern": "*.html", "ttl": 86400, "compress": True},
{"path_pattern": "*.css", "ttl": 604800, "compress": True},
{"path_pattern": "*.js", "ttl": 604800, "compress": True},
{"path_pattern": "*.jpg", "ttl": 2592000, "compress": False},
],
"functions": [
{"name": "landvex-handler", "status": "active", "purpose": "www-redirect + directory index"},
],
"invalidations": [
{"id": "I123456", "status": "Completed", "paths": ["/insights/*"], "created_at": "2026-07-04T06:15:00Z"},
{"id": "I123455", "status": "Completed", "paths": ["/"], "created_at": "2026-07-03T18:00:00Z"},
],
}
@router.post("/cloudfront/{distribution_id}/invalidate")
async def invalidate_cloudfront(
distribution_id: str,
paths: List[str],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Invalidera CloudFront cache."""
return {
"status": "invalidated",
"distribution_id": distribution_id,
"paths": paths,
"invalidation_id": f"I{datetime.now().strftime('%Y%m%d%H%M%S')}",
"created_at": datetime.now().isoformat(),
"created_by": str(current_user.id),
}
@router.get("/s3/buckets")
async def get_s3_buckets(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta S3 buckets."""
return {
"buckets": [
{"name": "landvex-prod", "region": "eu-north-1", "objects": 1250, "size_gb": 2.3, "versioning": "Enabled", "encryption": "AES256"},
{"name": "quixzoom-landing-prod", "region": "eu-north-1", "objects": 340, "size_gb": 0.8, "versioning": "Enabled", "encryption": "AES256"},
{"name": "landvex-backups", "region": "eu-north-1", "objects": 156, "size_gb": 4.5, "versioning": "Suspended", "encryption": "AES256"},
]
}
@router.get("/s3/buckets/{bucket_name}/objects")
async def get_s3_objects(
bucket_name: str,
prefix: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=1000),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Hämta objekt i en S3 bucket."""
objects = [
{"key": "index.html", "size": 12450, "last_modified": "2026-07-04T06:00:00Z", "etag": '"abc123"', "storage_class": "STANDARD"},
{"key": "insights/cost-of-outdated-data/index.html", "size": 8934, "last_modified": "2026-07-04T05:00:00Z", "etag": '"def456"', "storage_class": "STANDARD"},
{"key": "css/main.css", "size": 4521, "last_modified": "2026-07-01T00:00:00Z", "etag": '"ghi789"', "storage_class": "STANDARD"},
]
if prefix:
objects = [o for o in objects if o["key"].startswith(prefix)]
return {"bucket": bucket_name, "objects": objects[:limit], "total": len(objects)}
# ─── Backup & Restore ─────────────────────────────────────────────────────────
@router.get("/backups")
async def list_backups(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Lista alla backups."""
return {
"backups": [
{"id": "bk-20260704", "type": "automated", "status": "completed", "size_mb": 45.2, "created_at": "2026-07-04T03:00:00Z", "expires_at": "2026-08-04T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-04.sql.gz"},
{"id": "bk-20260703", "type": "automated", "status": "completed", "size_mb": 44.8, "created_at": "2026-07-03T03:00:00Z", "expires_at": "2026-08-03T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-03.sql.gz"},
{"id": "bk-20260702", "type": "automated", "status": "completed", "size_mb": 44.5, "created_at": "2026-07-02T03:00:00Z", "expires_at": "2026-08-02T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-02.sql.gz"},
{"id": "bk-20260701", "type": "manual", "status": "completed", "size_mb": 44.1, "created_at": "2026-07-01T12:00:00Z", "expires_at": None, "location": "s3://landvex-backups/manual/2026-07-01.sql.gz"},
],
"schedule": {
"daily": "03:00 UTC",
"weekly": "Sunday 03:00 UTC",
"retention_days": 30,
},
}
@router.post("/backups")
async def create_backup(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Skapa en manuell backup."""
return {
"status": "started",
"backup_id": f"bk-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"type": "manual",
"started_at": datetime.now().isoformat(),
"created_by": str(current_user.id),
}
@router.post("/backups/{backup_id}/restore")
async def restore_backup(
backup_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Återställ från backup."""
return {
"status": "started",
"backup_id": backup_id,
"started_at": datetime.now().isoformat(),
"warning": "This will overwrite current data. Ensure you have a fresh backup first.",
"created_by": str(current_user.id),
}
@router.delete("/backups/{backup_id}")
async def delete_backup(
backup_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Ta bort en backup."""
return {
"status": "deleted",
"backup_id": backup_id,
"deleted_by": str(current_user.id),
"deleted_at": datetime.now().isoformat(),
}
# ─── System Health ────────────────────────────────────────────────────────────
@router.get("/health")
async def get_detailed_health(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_manager),
):
"""Detaljerad system-hälsa."""
return {
"timestamp": datetime.now().isoformat(),
"overall": "healthy",
"services": {
"api": {"status": "healthy", "response_time_ms": 45, "last_check": datetime.now().isoformat()},
"database": {"status": "healthy", "response_time_ms": 12, "last_check": datetime.now().isoformat(), "connections": 8, "max_connections": 100},
"redis": {"status": "healthy", "response_time_ms": 3, "last_check": datetime.now().isoformat()},
"s3": {"status": "healthy", "response_time_ms": 89, "last_check": datetime.now().isoformat()},
"cloudfront": {"status": "healthy", "response_time_ms": 120, "last_check": datetime.now().isoformat()},
},
"resources": {
"cpu_percent": 23.5,
"memory_percent": 45.2,
"disk_percent": 62.1,
"disk_free_gb": 45.3,
},
"uptime": {
"api_seconds": 86400,
"database_seconds": 604800,
},
}