import os from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from fastapi.responses import FileResponse from sqlalchemy.orm import Session from database import get_db from utils.auth import verify_token from models import UserProfile from schemas import UserProfile as UserProfileSchema, UserProfileCreate from services import pki, generator from datetime import datetime router = APIRouter(dependencies=[Depends(verify_token)]) @router.get("/profiles", response_model=list[UserProfileSchema]) def list_profiles(db: Session = Depends(get_db)): # 1. Fetch profiles from DB profiles = db.query(UserProfile).all() # 2. Get PKI Data (Index mapping: CN -> Expiration Date) pki_data = pki.get_pki_index_data(db) now = datetime.utcnow() updated_profiles = [] for profile in profiles: # Sync expiration if available in PKI data if profile.username in pki_data: exp_date = pki_data[profile.username] # Update DB if different if profile.expiration_date != exp_date: profile.expiration_date = exp_date db.add(profile) # Calculate derived fields # 1. is_expired is_expired = False if profile.expiration_date: if now > profile.expiration_date: is_expired = True # 2. is_revoked # (Assuming status='revoked' in DB is the source of truth) is_revoked = profile.status == 'revoked' # 3. days_remaining (computed field) days_remaining = None if profile.expiration_date: delta = profile.expiration_date - now days_remaining = delta.days # Update DB fields for persistence if they differ if profile.is_expired != is_expired: profile.is_expired = is_expired db.add(profile) if profile.is_revoked != is_revoked: profile.is_revoked = is_revoked db.add(profile) # Inject computed fields for response schema # Since 'days_remaining' is not a DB column, we attach it to the object instance setattr(profile, 'days_remaining', days_remaining) updated_profiles.append(profile) db.commit() # Save any updates return updated_profiles @router.post("/profiles", response_model=UserProfileSchema) def create_profile( profile_in: UserProfileCreate, db: Session = Depends(get_db) ): # Check existing existing = db.query(UserProfile).filter(UserProfile.username == profile_in.username).first() if existing: raise HTTPException(status_code=400, detail="User already exists") # Build PKI try: pki.build_client(profile_in.username, db) except Exception as e: raise HTTPException(status_code=500, detail=f"PKI Build failed: {str(e)}") # Generate Config client_conf_dir = "client-config" os.makedirs(client_conf_dir, exist_ok=True) file_path = os.path.join(client_conf_dir, f"{profile_in.username}.ovpn") try: generator.generate_client_config(db, profile_in.username, file_path) except Exception as e: raise HTTPException(status_code=500, detail=f"Config Generation failed: {str(e)}") # Create DB Entry new_profile = UserProfile( username=profile_in.username, status="active", created_at=datetime.utcnow(), file_path=file_path # expired_at would be extracted from cert in a real robust implementation ) db.add(new_profile) db.commit() db.refresh(new_profile) return new_profile @router.delete("/profiles/{profile_id}") def revoke_profile(profile_id: int, db: Session = Depends(get_db)): profile = db.query(UserProfile).filter(UserProfile.id == profile_id).first() if not profile: raise HTTPException(status_code=404, detail="Profile not found") try: pki.revoke_client(profile.username, db) except Exception as e: # Log but maybe continue to update DB status? raise HTTPException(status_code=500, detail=f"Revocation failed: {str(e)}") profile.status = "revoked" profile.revoked_at = datetime.utcnow() db.commit() return {"message": f"Profile {profile.username} revoked"} @router.get("/profiles/{profile_id}/download") def download_profile(profile_id: int, db: Session = Depends(get_db)): profile = db.query(UserProfile).filter(UserProfile.id == profile_id).first() if not profile: raise HTTPException(status_code=404, detail="Profile not found") if not profile.file_path or not os.path.exists(profile.file_path): raise HTTPException(status_code=404, detail="Config file not found") return FileResponse(profile.file_path, filename=os.path.basename(profile.file_path))