feat(cra): scanner-repo→IACE-Projekt-Mapping persistieren (Pull-Flow) [migration-approved]
Ersetzt die ephemere Dropdown-Auswahl durch DB-Persistenz pro IACE-Projekt:
- Migration 156: compliance_cra_scanner_repo_map (tenant_id, iace_project_id PK,
scanner_repo_id). Additiv + idempotent.
- GET/PUT /v1/cra/scanner-repo-map/{iace_project_id} (Upsert/Clear).
- useCRA lädt das gespeicherte Repo beim Laden + persistiert bei Auswahl.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ function merge(live: any): CRADemo {
|
|||||||
export function useCRA(projectId?: string) {
|
export function useCRA(projectId?: string) {
|
||||||
const [data, setData] = useState<CRADemo | null>(null)
|
const [data, setData] = useState<CRADemo | null>(null)
|
||||||
const [live, setLive] = useState(false)
|
const [live, setLive] = useState(false)
|
||||||
const [scannerRepo, setScannerRepo] = useState('') // pull-flow: chosen scanner repo_id
|
const [scannerRepo, setScannerRepoState] = useState('') // pull-flow: chosen scanner repo_id
|
||||||
const [weights, setWeights] = useState<Weights>({})
|
const [weights, setWeights] = useState<Weights>({})
|
||||||
const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([])
|
const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([])
|
||||||
|
|
||||||
@@ -171,5 +171,25 @@ export function useCRA(projectId?: string) {
|
|||||||
return r.ok ? r.json() : null
|
return r.ok ? r.json() : null
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Pull-flow mapping: load the persisted scanner repo for this IACE project,
|
||||||
|
// and persist it on change (replaces the old ephemeral dropdown state).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return
|
||||||
|
fetch(`/api/v1/cra/scanner-repo-map/${projectId}`)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((j) => { if (j?.scanner_repo_id) setScannerRepoState(j.scanner_repo_id) })
|
||||||
|
.catch(() => {})
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
|
const setScannerRepo = useCallback((v: string) => {
|
||||||
|
setScannerRepoState(v)
|
||||||
|
if (projectId) {
|
||||||
|
fetch(`/api/v1/cra/scanner-repo-map/${projectId}`, {
|
||||||
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ scanner_repo_id: v }),
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot, scannerRepo, setScannerRepo }
|
return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot, scannerRepo, setScannerRepo }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,3 +68,49 @@ async def by_iace(iace_project_id: str, tenant_id: str = Depends(get_tenant_id))
|
|||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ScannerRepoMap(BaseModel):
|
||||||
|
scanner_repo_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scanner-repo-map/{iace_project_id}")
|
||||||
|
async def get_scanner_repo_map(iace_project_id: str, tenant_id: str = Depends(get_tenant_id)):
|
||||||
|
"""The scanner repo persisted for this IACE project (pull-flow), or empty."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
row = db.execute(text("""
|
||||||
|
SELECT scanner_repo_id FROM compliance_cra_scanner_repo_map
|
||||||
|
WHERE tenant_id = :tid AND iace_project_id = CAST(:p AS uuid)
|
||||||
|
"""), {"tid": tenant_id, "p": iace_project_id}).fetchone()
|
||||||
|
return {"scanner_repo_id": row[0] if row else ""}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/scanner-repo-map/{iace_project_id}")
|
||||||
|
async def put_scanner_repo_map(iace_project_id: str, body: ScannerRepoMap,
|
||||||
|
tenant_id: str = Depends(get_tenant_id)):
|
||||||
|
"""Upsert (or clear, on empty) the scanner repo for this IACE project."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
if body.scanner_repo_id:
|
||||||
|
db.execute(text("""
|
||||||
|
INSERT INTO compliance_cra_scanner_repo_map
|
||||||
|
(tenant_id, iace_project_id, scanner_repo_id, updated_at)
|
||||||
|
VALUES (:tid, CAST(:p AS uuid), :r, NOW())
|
||||||
|
ON CONFLICT (tenant_id, iace_project_id)
|
||||||
|
DO UPDATE SET scanner_repo_id = EXCLUDED.scanner_repo_id, updated_at = NOW()
|
||||||
|
"""), {"tid": tenant_id, "p": iace_project_id, "r": body.scanner_repo_id})
|
||||||
|
else:
|
||||||
|
db.execute(text("""
|
||||||
|
DELETE FROM compliance_cra_scanner_repo_map
|
||||||
|
WHERE tenant_id = :tid AND iace_project_id = CAST(:p AS uuid)
|
||||||
|
"""), {"tid": tenant_id, "p": iace_project_id})
|
||||||
|
db.commit()
|
||||||
|
return {"iace_project_id": iace_project_id, "scanner_repo_id": body.scanner_repo_id}
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Migration 156: persist the scanner repo chosen per IACE project for the
|
||||||
|
-- CRA/Cyber pull-flow (replaces the ephemeral UI dropdown selection). Keyed by
|
||||||
|
-- IACE project id so it works for any project, independent of a linked CRA
|
||||||
|
-- project. Additive + idempotent.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS compliance_cra_scanner_repo_map (
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
iace_project_id UUID NOT NULL,
|
||||||
|
scanner_repo_id TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (tenant_id, iace_project_id)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user