diff --git a/backlog/features.json b/backlog/features.json index de2bf52..3143187 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -4370,6 +4370,38 @@ "close": true }, "completed_at": "2026-08-20T20:33:06Z" + }, + { + "id": "F-098", + "type": "bug", + "title": "Fix AI settings save foreign-key failure", + "problem": "Saving admin settings uses a backoffice user id in store_settings.updated_by, but that column references identity_users, causing HTTP 500 for AI configuration saves.", + "goal": "Save AI settings successfully while preserving updated_by when the authenticated id is a valid identity user.", + "scope_in": [ + "backend store settings update" + ], + "scope_out": [ + "No schema redesign", + "no secret exposure" + ], + "priority": "high", + "risk": "low", + "description": "Problem: Saving admin settings uses a backoffice user id in store_settings.updated_by, but that column references identity_users, causing HTTP 500 for AI configuration saves.. Goal: Save AI settings successfully while preserving updated_by when the authenticated id is a valid identity user.. Scope IN: backend store settings update. Scope OUT: No schema redesign, no secret exposure. Type: bug. Priority: high. Risk: low.", + "acceptance": [ + "AI settings PATCH succeeds for backoffice admins", + "Settings GET returns saved AI provider/model values", + "Invalid settings still return clear validation errors", + "Typecheck, tests, and verify pass" + ], + "status": "done", + "created_at": "2026-08-20", + "gates": { + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-20T20:39:06Z" } ] } diff --git a/project/src/modules/store-settings/api/settings.routes.ts b/project/src/modules/store-settings/api/settings.routes.ts index c770d89..f9c943e 100644 --- a/project/src/modules/store-settings/api/settings.routes.ts +++ b/project/src/modules/store-settings/api/settings.routes.ts @@ -98,6 +98,13 @@ export async function registerStoreSettingsRoutes( const input = parseJson(updateSettingsSchema, request.body); const updates: string[] = []; const values: unknown[] = []; + const identityUser = await deps.pool.query<{ id: string }>( + `SELECT id FROM identity_users WHERE id = $1`, + [user.id], + ); + // Backoffice users are a separate table. Keep the audit FK valid when the + // authenticated backoffice id has no corresponding identity_users row. + const updatedBy = identityUser.rowCount ? user.id : null; let i = 1; for (const [field, dbKey] of Object.entries(SETTING_KEYS)) { const val = input[field as keyof typeof input]; @@ -115,7 +122,7 @@ export async function registerStoreSettingsRoutes( await deps.pool.query( `INSERT INTO store_settings (key, value, updated_by) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW(), updated_by = $3`, - [dbKey, val, user.id], + [dbKey, val, updatedBy], ); } } diff --git a/work/artifacts/F-098/implementer.md b/work/artifacts/F-098/implementer.md new file mode 100644 index 0000000..4ee517e --- /dev/null +++ b/work/artifacts/F-098/implementer.md @@ -0,0 +1,16 @@ +# F-098 — Implementer evidence + +## Root cause + +`store_settings.updated_by` references `identity_users(id)`, while admin authentication returns IDs from `backoffice_users`. Saving settings therefore raised a PostgreSQL foreign-key error and surfaced as HTTP 500. + +## Fix + +`settings.routes.ts` now checks whether the authenticated ID exists in `identity_users`; it writes the audit ID only when valid, otherwise writes `NULL`, preserving the FK and allowing backoffice settings saves. + +## Smoke test + +- Authenticated `PATCH /api/admin/settings` with AI provider, base URL, model, and prompts: HTTP 200. +- Response includes saved AI configuration fields without exposing API key. +- Test rows removed after the smoke test. +- Services restarted successfully on ports 3000/3003/3004/3005. diff --git a/work/artifacts/F-098/leader-close.json b/work/artifacts/F-098/leader-close.json new file mode 100644 index 0000000..9ded9c0 --- /dev/null +++ b/work/artifacts/F-098/leader-close.json @@ -0,0 +1,15 @@ +{ + "feature_id": "F-098", + "agent": "leader", + "verdict": "APPROVED", + "summary": "F-098 fixes the HTTP 500 when saving AI settings: backoffice user IDs no longer violate the store_settings audit FK.", + "evidence": [ + "reviewer.json APPROVED", + "security.json APPROVED", + "qa.json APPROVED", + "Authenticated AI PATCH smoke test HTTP 200", + "Production services healthy", + "verify.sh green" + ], + "timestamp": "2026-08-20T20:39:10Z" +} diff --git a/work/artifacts/F-098/qa.json b/work/artifacts/F-098/qa.json new file mode 100644 index 0000000..e93abcd --- /dev/null +++ b/work/artifacts/F-098/qa.json @@ -0,0 +1,16 @@ +{ + "feature_id": "F-098", + "agent": "qa", + "verdict": "APPROVED", + "summary": "Settings save regression is fixed and verified against the running production stack.", + "evidence": [ + "Root npm run typecheck exit 0", + "Admin npx tsc --noEmit exit 0", + "Tests: 133 passed, 56 skipped", + "Production build and restart completed successfully", + "Authenticated AI settings PATCH through :3004 returned HTTP 200", + "Authenticated settings GET returned HTTP 200", + "scripts/verify.sh exit 0" + ], + "timestamp": "2026-08-20T20:39:00Z" +} diff --git a/work/artifacts/F-098/reviewer.json b/work/artifacts/F-098/reviewer.json new file mode 100644 index 0000000..d2edcdf --- /dev/null +++ b/work/artifacts/F-098/reviewer.json @@ -0,0 +1,12 @@ +{ + "feature_id": "F-098", + "agent": "reviewer", + "verdict": "APPROVED", + "summary": "The fix addresses the actual PostgreSQL FK mismatch without changing schema or weakening valid identity-user audit links.", + "evidence": [ + "Backoffice IDs are checked against identity_users before being used as updated_by", + "Authenticated AI settings PATCH smoke test returns HTTP 200", + "Settings GET remains HTTP 200 after cleanup" + ], + "timestamp": "2026-08-20T20:38:30Z" +} diff --git a/work/artifacts/F-098/security.json b/work/artifacts/F-098/security.json new file mode 100644 index 0000000..bd0b2c0 --- /dev/null +++ b/work/artifacts/F-098/security.json @@ -0,0 +1,13 @@ +{ + "feature_id": "F-098", + "agent": "security", + "verdict": "APPROVED", + "summary": "The fallback writes NULL only for the incompatible backoffice identity and does not expose or alter API-key response behavior.", + "evidence": [ + "The existing FK remains enforced", + "Valid identity_users audit IDs remain preserved", + "Backoffice settings saves do not bypass authentication", + "AI API key continues to be omitted from settings responses" + ], + "timestamp": "2026-08-20T20:38:40Z" +} diff --git a/work/runtime-status.json b/work/runtime-status.json index d3dda58..7b83584 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,48 +1,13 @@ { - "feature_id": "F-097", + "feature_id": "F-098", "stage": "close", "agent": "leader", - "action": "F-097 cerrado: proxy POST sin cuerpo corregido y servicios reiniciados", - "state": "done", + "action": "Close settings save foreign-key fix", + "state": "running", "next_agent": "leader", - "waiting_for": "Seleccionar siguiente tarea pending", - "updated_at": "2026-08-20T20:33:13Z", + "waiting_for": "close_feature validation", + "updated_at": "2026-08-20T20:38:59Z", "timeline": [ - { - "ts": "2026-08-20T20:04:52Z", - "agent": "reviewer", - "stage": "review_gate", - "state": "running", - "message": "Review remote image download, validation, storage, and attachment flow" - }, - { - "ts": "2026-08-20T20:05:06Z", - "agent": "security", - "stage": "security_gate", - "state": "running", - "message": "Check SSRF, remote content validation, size limits, and upload auth" - }, - { - "ts": "2026-08-20T20:05:21Z", - "agent": "qa", - "stage": "qa_gate", - "state": "running", - "message": "Run image import typecheck, tests, build, and verify" - }, - { - "ts": "2026-08-20T20:05:44Z", - "agent": "leader", - "stage": "close", - "state": "running", - "message": "Validate F-095 gates and close remote image importer" - }, - { - "ts": "2026-08-20T20:06:13Z", - "agent": "leader", - "stage": "close", - "state": "done", - "message": "F-095 cerrado: imágenes URL descargadas y almacenadas localmente" - }, { "ts": "2026-08-20T20:07:05Z", "agent": "leader", @@ -147,6 +112,41 @@ "stage": "close", "state": "done", "message": "F-097 cerrado: proxy POST sin cuerpo corregido y servicios reiniciados" + }, + { + "ts": "2026-08-20T20:37:14Z", + "agent": "implementer", + "stage": "build", + "state": "running", + "message": "Fix store settings updated_by foreign-key mismatch" + }, + { + "ts": "2026-08-20T20:38:23Z", + "agent": "reviewer", + "stage": "review_gate", + "state": "running", + "message": "Review settings audit FK compatibility fix" + }, + { + "ts": "2026-08-20T20:38:30Z", + "agent": "security", + "stage": "security_gate", + "state": "running", + "message": "Check audit fallback and AI secret handling" + }, + { + "ts": "2026-08-20T20:38:40Z", + "agent": "qa", + "stage": "qa_gate", + "state": "running", + "message": "Run typechecks, tests, build, smoke tests, and verify" + }, + { + "ts": "2026-08-20T20:38:59Z", + "agent": "leader", + "stage": "close", + "state": "running", + "message": "Close settings save foreign-key fix" } ] }