Fase 0 complete: extract all business logic, auth, database, and 176 endpoints from monolithic app.py into dedicated modules. Structure: - app.py: FastAPI hub (CORS, routers, startup/shutdown) - models.py: 13 Pydantic request models - utils.py: shared utilities (embed status, upload validation, process registries) - config.py: constants, env vars, model catalogs - auth/: crypto, jwt_auth, oidc, rate_limit - database/: db(), init_db(), schema DDL - services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp - routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer, genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine) Also: README updated with OCIR auth instructions for manual docker run. 86 tests passing.
301 lines
17 KiB
Python
301 lines
17 KiB
Python
"""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
|