"""Settings routes — audit log, config logs, chat logs, app settings, system prompts, config export/import.""" import uuid import json import base64 from datetime import datetime from pathlib import Path from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File from fastapi.responses import StreamingResponse from config import VERSION, OCI_DIR, log from database import db from auth.jwt_auth import current_user, require, _audit from auth.crypto import _enc, _dec, _mask, _safe_dec router = APIRouter() # ── Audit Log ──────────────────────────────────────────────────────────────── @router.get("/api/audit-log") async def audit_log(skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), u=Depends(current_user)): with db() as c: if u["role"] == "admin": rows = c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ? OFFSET ?", (limit, skip)).fetchall() else: rows = c.execute("SELECT * FROM audit_log WHERE user_id=? ORDER BY created_at DESC LIMIT ? OFFSET ?", (u["id"], limit, skip)).fetchall() return [dict(r) for r in rows] # ── Config Logs ────────────────────────────────────────────────────────────── @router.get("/api/config-logs") async def get_config_logs( config_type: str = Query(None), config_id: str = Query(None), severity: str = Query(None), limit: int = Query(50, le=200), u=Depends(current_user) ): query = "SELECT * FROM config_logs WHERE 1=1" params = [] if config_type: query += " AND config_type=?"; params.append(config_type) if config_id: query += " AND config_id=?"; params.append(config_id) if severity: query += " AND severity=?"; params.append(severity) if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"]) query += " ORDER BY created_at DESC LIMIT ?" params.append(limit) with db() as c: rows = c.execute(query, params).fetchall() return [dict(r) for r in rows] # ── Chat Logs ──────────────────────────────────────────────────────────────── @router.get("/api/chat-logs") async def get_chat_logs( severity: str = Query(None), session_id: str = Query(None), limit: int = Query(50, le=200), u=Depends(current_user) ): query = "SELECT * FROM chat_logs WHERE 1=1" params = [] if severity: query += " AND severity=?"; params.append(severity) if session_id: query += " AND session_id=?"; params.append(session_id) if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"]) query += " ORDER BY created_at DESC LIMIT ?" params.append(limit) with db() as c: rows = c.execute(query, params).fetchall() return [dict(r) for r in rows] # ── App Settings ───────────────────────────────────────────────────────────── _SENSITIVE_SETTING_PREFIXES = ("oidc_", "secret_", "app_secret", "encryption_") @router.get("/api/settings/{key}") async def get_setting(key: str, u=Depends(current_user)): if any(key.startswith(p) or key == p for p in _SENSITIVE_SETTING_PREFIXES) and u["role"] != "admin": raise HTTPException(403, "Acesso negado a configuração sensível") with db() as c: row = c.execute("SELECT value FROM app_settings WHERE key=?", (key,)).fetchone() val = row["value"] if row else "" if key == "oidc_client_secret" and val: val = _mask(_safe_dec(val)) return {"key": key, "value": val} @router.put("/api/settings/{key}") async def put_setting(key: str, body: dict, u=Depends(require("admin"))): value = body.get("value", "") if key == "oidc_client_secret" and value and "\u2022" in value: return {"key": key, "value": value} if key == "oidc_client_secret" and value: value = _enc(value) with db() as c: c.execute("INSERT INTO app_settings (key,value,updated_at) VALUES (?,?,datetime('now')) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at", (key, value)) return {"key": key, "value": value} @router.get("/api/settings/timezone/options") async def timezone_options(u=Depends(current_user)): """List common timezone options.""" return {"timezones": [ "America/Sao_Paulo", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Toronto", "America/Mexico_City", "America/Bogota", "America/Argentina/Buenos_Aires", "America/Santiago", "Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Madrid", "Europe/Lisbon", "Asia/Tokyo", "Asia/Shanghai", "Asia/Singapore", "Asia/Kolkata", "Asia/Dubai", "Australia/Sydney", "Pacific/Auckland", "UTC", ]} @router.put("/api/users/me/timezone") async def set_user_timezone(body: dict, u=Depends(current_user)): """Set the current user's timezone.""" tz = body.get("timezone", "").strip() if not tz: raise HTTPException(400, "timezone is required") try: import zoneinfo zoneinfo.ZoneInfo(tz) except Exception: raise HTTPException(400, f"Invalid timezone: {tz}") with db() as c: c.execute("UPDATE users SET timezone=? WHERE id=?", (tz, u["id"])) _audit(u["id"], u["username"], "set_timezone", tz) return {"ok": True, "timezone": tz} @router.get("/api/users/me/timezone") async def get_user_timezone(u=Depends(current_user)): """Get the current user's timezone.""" return {"timezone": u.get("timezone", "America/Sao_Paulo")} # ── System Prompts ─────────────────────────────────────────────────────────── @router.get("/api/prompts/{agent}") async def list_prompts(agent: str, u=Depends(current_user)): with db() as c: rows = c.execute("SELECT * FROM system_prompts WHERE agent=? ORDER BY is_active DESC, created_at DESC", (agent,)).fetchall() return [dict(r) for r in rows] @router.post("/api/prompts") async def save_prompt(body: dict, u=Depends(require("admin"))): agent = body.get("agent", "chat") with db() as c: count = c.execute("SELECT COUNT(*) FROM system_prompts WHERE agent=?", (agent,)).fetchone()[0] if count >= 10: raise HTTPException(400, "Limite de 10 prompts atingido. Exclua um antes de criar outro.") pid = str(uuid.uuid4()) name = body.get("name", "Sem nome") content = body.get("content", "") is_active = body.get("is_active", False) with db() as c: if is_active: c.execute("UPDATE system_prompts SET is_active=0 WHERE agent=?", (agent,)) c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active) VALUES (?,?,?,?,?)", (pid, name, agent, content, int(is_active))) return {"id": pid} @router.put("/api/prompts/{pid}") async def update_prompt(pid: str, body: dict, u=Depends(require("admin"))): with db() as c: existing = c.execute("SELECT * FROM system_prompts WHERE id=?", (pid,)).fetchone() if not existing: raise HTTPException(404) name = body.get("name", existing["name"]) content = body.get("content", existing["content"]) is_active = body.get("is_active", existing["is_active"]) with db() as c: if is_active: c.execute("UPDATE system_prompts SET is_active=0 WHERE agent=?", (existing["agent"],)) c.execute("UPDATE system_prompts SET name=?,content=?,is_active=? WHERE id=?", (name, content, int(is_active), pid)) return {"id": pid} @router.delete("/api/prompts/{pid}") async def delete_prompt(pid: str, u=Depends(require("admin"))): with db() as c: c.execute("DELETE FROM system_prompts WHERE id=?", (pid,)) return {"ok": True} # ── Export / Import Configuration ──────────────────────────────────────────── @router.get("/api/config/export") async def export_config(u=Depends(require("admin"))): """Export configurations as JSON. Excludes sensitive credentials (private keys, passwords, secrets).""" _OCI_SAFE_COLS = "id,user_id,tenancy_name,region,compartment_id,auth_type,ssh_public_key,created_at" _ADB_SAFE_COLS = "id,user_id,config_name,dsn,username,table_name,use_mtls,is_active,is_global,genai_config_id,embedding_model_id,created_at" data = {"version": VERSION, "exported_at": datetime.now().isoformat()} with db() as c: # OCI configs (no private keys, no encrypted OCIDs, no passphrases) data["oci_configs"] = [dict(r) for r in c.execute(f"SELECT {_OCI_SAFE_COLS} FROM oci_configs").fetchall()] # GenAI configs (no secrets — model info only) data["genai_configs"] = [dict(r) for r in c.execute("SELECT * FROM genai_configs").fetchall()] # MCP servers data["mcp_servers"] = [dict(r) for r in c.execute("SELECT * FROM mcp_servers").fetchall()] # System prompts data["system_prompts"] = [dict(r) for r in c.execute("SELECT * FROM system_prompts").fetchall()] # App settings (exclude sensitive keys) data["app_settings"] = [dict(r) for r in c.execute("SELECT * FROM app_settings").fetchall() if not any(r["key"].startswith(p) for p in _SENSITIVE_SETTING_PREFIXES)] # ADB vector configs (no passwords, no wallet passwords) data["adb_vector_configs"] = [dict(r) for r in c.execute(f"SELECT {_ADB_SAFE_COLS} FROM adb_vector_configs").fetchall()] # ADB vector tables data["adb_vector_tables"] = [dict(r) for r in c.execute("SELECT * FROM adb_vector_tables").fetchall()] return StreamingResponse( iter([json.dumps(data, indent=2, ensure_ascii=False)]), media_type="application/json", headers={"Content-Disposition": f'attachment; filename="oci-cis-agent-config-{datetime.now().strftime("%Y%m%d-%H%M%S")}.json"'} ) @router.post("/api/config/import") async def import_config(file: UploadFile = File(...), u=Depends(require("admin"))): """Import configurations from exported JSON. Merges by ID (skip existing).""" from utils import validate_upload await validate_upload(file, {".json"}) file.file.seek(0) content = await file.read() try: data = json.loads(content) except json.JSONDecodeError: raise HTTPException(400, "Arquivo JSON inválido") counts = {"oci_configs": 0, "genai_configs": 0, "mcp_servers": 0, "system_prompts": 0, "app_settings": 0, "adb_vector_configs": 0, "adb_vector_tables": 0, "skipped": 0} with db() as c: # OCI configs for cfg in data.get("oci_configs", []): if c.execute("SELECT 1 FROM oci_configs WHERE id=?", (cfg["id"],)).fetchone(): counts["skipped"] += 1; continue # Import metadata only — credentials must be reconfigured manually cdir = OCI_DIR / cfg["id"]; cdir.mkdir(parents=True, exist_ok=True) kp = cdir / "oci_api_key.pem" # Restore private key only if present in legacy export format if cfg.get("_private_key_b64"): kp.write_bytes(base64.b64decode(cfg["_private_key_b64"])); kp.chmod(0o600) cfg_file = cdir / "config" cfg_content = (f"[DEFAULT]\nuser={_dec(cfg['user_ocid']) if cfg.get('user_ocid') else ''}\n" f"fingerprint={_dec(cfg['fingerprint']) if cfg.get('fingerprint') else ''}\n" f"tenancy={_dec(cfg['tenancy_ocid']) if cfg.get('tenancy_ocid') else ''}\n" f"region={cfg.get('region','')}\nkey_file={kp}\n") if cfg.get("_key_passphrase"): cfg_content += f"pass_phrase={cfg['_key_passphrase']}\n" cfg_file.write_text(cfg_content); cfg_file.chmod(0o600) c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)", (cfg["id"], cfg.get("user_id", u["id"]), cfg.get("tenancy_name",""), _enc(cfg["tenancy_ocid"]) if cfg.get("tenancy_ocid") else "", _enc(cfg["user_ocid"]) if cfg.get("user_ocid") else "", _enc(cfg["fingerprint"]) if cfg.get("fingerprint") else "", cfg.get("region",""), str(kp), _enc(cfg["compartment_id"]) if cfg.get("compartment_id") else None, cfg.get("created_at"))) counts["oci_configs"] += 1 # GenAI configs for cfg in data.get("genai_configs", []): if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone(): counts["skipped"] += 1; continue c.execute("INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k,frequency_penalty,presence_penalty,reasoning_effort,is_default,system_prompt,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (cfg["id"], cfg.get("user_id", u["id"]), cfg.get("name",""), cfg.get("oci_config_id",""), cfg.get("model_id",""), cfg.get("model_ocid"), cfg.get("compartment_id",""), cfg.get("genai_region",""), cfg.get("endpoint",""), cfg.get("serving_type","ON_DEMAND"), cfg.get("dedicated_endpoint_id"), cfg.get("temperature",1), cfg.get("max_tokens",6000), cfg.get("top_p",0.95), cfg.get("top_k",1), cfg.get("frequency_penalty",0), cfg.get("presence_penalty",0), cfg.get("reasoning_effort"), cfg.get("is_default",0), cfg.get("system_prompt",""), cfg.get("created_at"))) counts["genai_configs"] += 1 # MCP servers for srv in data.get("mcp_servers", []): if c.execute("SELECT 1 FROM mcp_servers WHERE id=?", (srv["id"],)).fetchone(): counts["skipped"] += 1; continue c.execute("INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id,is_active,is_global,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (srv["id"], srv.get("user_id", u["id"]), srv.get("name",""), srv.get("description"), srv.get("server_type","stdio"), srv.get("command"), srv.get("args"), srv.get("env_vars"), srv.get("url"), srv.get("module_path"), srv.get("tools"), srv.get("linked_adb_id"), srv.get("is_active",1), srv.get("is_global",0), srv.get("created_at"))) counts["mcp_servers"] += 1 # System prompts for p in data.get("system_prompts", []): if c.execute("SELECT 1 FROM system_prompts WHERE id=?", (p["id"],)).fetchone(): counts["skipped"] += 1; continue c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active,created_at) VALUES (?,?,?,?,?,?)", (p["id"], p.get("name",""), p.get("agent","chat"), p.get("content",""), p.get("is_active",0), p.get("created_at"))) counts["system_prompts"] += 1 # App settings for s in data.get("app_settings", []): if any(s["key"].startswith(p) for p in _SENSITIVE_SETTING_PREFIXES): counts["skipped"] += 1; continue c.execute("INSERT INTO app_settings (key,value,updated_at) VALUES (?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at", (s["key"], s.get("value",""), s.get("updated_at"))) counts["app_settings"] += 1 # ADB vector configs for cfg in data.get("adb_vector_configs", []): if c.execute("SELECT 1 FROM adb_vector_configs WHERE id=?", (cfg["id"],)).fetchone(): counts["skipped"] += 1; continue c.execute("INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_dir,wallet_password_enc,table_name,use_mtls,is_active,is_global,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", (cfg["id"], cfg.get("user_id", u["id"]), cfg.get("config_name",""), cfg.get("dsn",""), cfg.get("username",""), cfg.get("password_enc",""), cfg.get("wallet_dir"), cfg.get("wallet_password_enc"), cfg.get("table_name","CIS_EMBEDDINGS"), cfg.get("use_mtls",1), cfg.get("is_active",1), cfg.get("is_global",0), cfg.get("created_at"))) counts["adb_vector_configs"] += 1 # ADB vector tables for t in data.get("adb_vector_tables", []): if c.execute("SELECT 1 FROM adb_vector_tables WHERE id=?", (t["id"],)).fetchone(): counts["skipped"] += 1; continue c.execute("INSERT INTO adb_vector_tables (id,adb_config_id,table_name,description,is_active,created_at) VALUES (?,?,?,?,?,?)", (t["id"], t.get("adb_config_id",""), t.get("table_name",""), t.get("description",""), t.get("is_active",1), t.get("created_at"))) counts["adb_vector_tables"] += 1 _audit(u["id"], u["username"], "import_config", "bulk", json.dumps(counts)) return counts