diff --git a/backend/app.py b/backend/app.py index 6823871..f70c2f2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -17,7 +17,7 @@ from fastapi import ( Query, Body, BackgroundTasks ) from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, FileResponse +from fastapi.responses import JSONResponse, FileResponse, StreamingResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel import jwt as pyjwt @@ -4336,6 +4336,133 @@ async def delete_prompt(pid: str, u=Depends(require("admin"))): c.execute("DELETE FROM system_prompts WHERE id=?", (pid,)) return {"ok": True} +# ── Export / Import Configuration ───────────────────────────────────────────── +@app.get("/api/config/export") +async def export_config(u=Depends(require("admin"))): + """Export all configurations as JSON (OCI configs with keys, GenAI, MCP, prompts, settings).""" + data = {"version": VERSION, "exported_at": datetime.now().isoformat()} + with db() as c: + # OCI configs + oci_rows = c.execute("SELECT * FROM oci_configs").fetchall() + oci_cfgs = [] + for r in oci_rows: + cfg = dict(r) + # Include private key content (base64) + kp = Path(cfg.get("key_file_path", "")) + if kp.exists(): + cfg["_private_key_b64"] = base64.b64encode(kp.read_bytes()).decode() + # Include passphrase from config file if exists + cfg_file = kp.parent / "config" if kp.parent.exists() else None + if cfg_file and cfg_file.exists(): + for line in cfg_file.read_text().splitlines(): + if line.startswith("pass_phrase="): + cfg["_key_passphrase"] = line.split("=", 1)[1] + oci_cfgs.append(cfg) + data["oci_configs"] = oci_cfgs + # GenAI configs + data["genai_configs"] = [dict(r) for r in c.execute("SELECT * FROM genai_configs").fetchall()] + # MCP servers (exclude system-auto-registered if tools are empty) + 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 + data["app_settings"] = [dict(r) for r in c.execute("SELECT * FROM app_settings").fetchall()] + # ADB vector configs + adb_rows = c.execute("SELECT * FROM adb_vector_configs").fetchall() + data["adb_vector_configs"] = [dict(r) for r in adb_rows] + # 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"'} + ) + + +@app.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).""" + 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 + # Restore private key file + cdir = OCI_DIR / cfg["id"]; cdir.mkdir(parents=True, exist_ok=True) + kp = cdir / "oci_api_key.pem" + if cfg.get("_private_key_b64"): + kp.write_bytes(base64.b64decode(cfg["_private_key_b64"])); kp.chmod(0o600) + # Write config file + 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",""), cfg.get("tenancy_ocid",""), cfg.get("user_ocid",""), + cfg.get("fingerprint",""), cfg.get("region",""), str(kp), cfg.get("compartment_id"), 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,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("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,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("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", []): + 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,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("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 + + # ── CIS Engine Version Management ───────────────────────────────────────────── CIS_REPORTS_PATH = Path("/app/cis_reports.py") CIS_GITHUB_RAW = "https://raw.githubusercontent.com/oci-landing-zones/oci-cis-landingzone-quickstart/main/scripts/cis_reports.py" diff --git a/frontend/index.html b/frontend/index.html index eecb398..ea4e257 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1824,7 +1824,15 @@ ${ddItems||'
Nenhuma
`:''}`} function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()} function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null; - return`
☁️ Credenciais Registradas
+ return`
+
⚙️ Configuração do Sistema
+
+ + + +
+
+
☁️ Credenciais Registradas
${S.ociCfg.map(c=>` @@ -1862,6 +1870,39 @@ async function sOci(){const tn=document.getElementById('otn').value; const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om',' Salvando credencial...','i'); try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST'; await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}} +async function cfgExport(){ + try{ + const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}}); + if(!resp.ok)throw new Error('Erro ao exportar'); + const blob=await resp.blob(); + const url=URL.createObjectURL(blob); + const a=document.createElement('a');a.href=url; + a.download=resp.headers.get('content-disposition')?.match(/filename="(.+)"/)?.[1]||'config-export.json'; + a.click();URL.revokeObjectURL(url); + sm('cfgMsg','✅ Configuração exportada com sucesso!','s'); + }catch(e){sm('cfgMsg','❌ '+e.message,'e')} +} +async function cfgImport(input){ + const file=input.files[0];if(!file)return; + try{ + const fd=new FormData();fd.append('file',file); + const resp=await fetch(API+'/config/import',{method:'POST',headers:S.token?{'Authorization':'Bearer '+S.token}:{},body:fd}); + if(!resp.ok){const err=await resp.json();throw new Error(err.detail||'Erro ao importar')} + const d=await resp.json(); + const parts=[]; + if(d.oci_configs)parts.push(d.oci_configs+' OCI configs'); + if(d.genai_configs)parts.push(d.genai_configs+' GenAI configs'); + if(d.mcp_servers)parts.push(d.mcp_servers+' MCP servers'); + if(d.system_prompts)parts.push(d.system_prompts+' prompts'); + if(d.app_settings)parts.push(d.app_settings+' settings'); + if(d.adb_vector_configs)parts.push(d.adb_vector_configs+' ADB configs'); + if(d.skipped)parts.push(d.skipped+' ignorados (já existem)'); + sm('cfgMsg','✅ Importado: '+parts.join(', '),'s'); + [S.ociCfg,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/genai/configs'),$api('/mcp/servers')]); + R(); + }catch(e){sm('cfgMsg','❌ '+e.message,'e')} + finally{input.value=''} +} async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?'✅ Conexão OK!':'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}} async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
TenancyRegionDetalhesAções
${c.tenancy_name}
${c.created_at}
${c.region} 🔑 ••••••••••••••••••••
🏢 ${c.tenancy_ocid}
👤 ${c.user_ocid}
📦 ${c.compartment_id||'—'}