diff --git a/Cyrano/migrations/003_drop_practice_profile_fk.sql b/Cyrano/migrations/003_drop_practice_profile_fk.sql new file mode 100644 index 00000000..048bed89 --- /dev/null +++ b/Cyrano/migrations/003_drop_practice_profile_fk.sql @@ -0,0 +1,17 @@ +-- Migration 003: Drop foreign-key constraint on practice_profiles.user_id +-- +-- The practice_profiles table is keyed by the JWT user-id (an integer), not a +-- row in the wellness `users` table. Keeping a FK reference to users(id) +-- causes INSERT failures whenever the authenticated user's id has no matching +-- row in `users` (e.g. external-auth users, test users, CI fixtures). +-- Removing the constraint keeps the column semantics intact while letting the +-- onboarding flow work for any valid authenticated session. +-- +-- Referential integrity is enforced at the application layer: +-- • authenticateJWT middleware validates the JWT before any route handler runs +-- • upsertPracticeProfile() rejects non-numeric user IDs (parseInt check) +-- • Row ownership is enforced by always reading userId from req.user (JWT), +-- never trusting a caller-supplied userId in the request body + +ALTER TABLE practice_profiles + DROP CONSTRAINT IF EXISTS practice_profiles_user_id_fkey; diff --git a/Cyrano/src/routes/anonymization.ts b/Cyrano/src/routes/anonymization.ts index ff3ddab2..8e70762d 100644 --- a/Cyrano/src/routes/anonymization.ts +++ b/Cyrano/src/routes/anonymization.ts @@ -124,7 +124,7 @@ router.post('/anonymization/terms', async (req: Request, res: Response) => { if (!parsed.success) { return res.status(400).json({ success: false, - error: parsed.error.errors.map(e => e.message).join(', '), + error: parsed.error.issues.map(e => e.message).join(', '), }); } @@ -142,7 +142,7 @@ router.post('/anonymization/terms', async (req: Request, res: Response) => { */ router.delete('/anonymization/terms/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = String(req.params['id']); const removed = clientAnonymizationService.removeCustomTerm(id); if (!removed) { return res.status(404).json({ success: false, error: 'Custom term not found' }); @@ -183,7 +183,7 @@ router.post('/anonymization/exceptions', async (req: Request, res: Response) => if (!parsed.success) { return res.status(400).json({ success: false, - error: parsed.error.errors.map(e => e.message).join(', '), + error: parsed.error.issues.map(e => e.message).join(', '), }); } @@ -201,7 +201,7 @@ router.post('/anonymization/exceptions', async (req: Request, res: Response) => */ router.delete('/anonymization/exceptions/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = String(req.params['id']); const removed = clientAnonymizationService.removeException(id); if (!removed) { return res.status(404).json({ success: false, error: 'Exception not found' }); @@ -231,7 +231,7 @@ router.post('/anonymization/preview', (req: Request, res: Response) => { if (!parsed.success) { return res.status(400).json({ success: false, - error: parsed.error.errors.map(e => e.message).join(', '), + error: parsed.error.issues.map(e => e.message).join(', '), }); } diff --git a/Cyrano/src/schema-library.ts b/Cyrano/src/schema-library.ts index 2538b188..019ce173 100644 --- a/Cyrano/src/schema-library.ts +++ b/Cyrano/src/schema-library.ts @@ -13,7 +13,7 @@ import { users } from './schema.js'; */ export const practiceProfiles = pgTable('practice_profiles', { id: uuid('id').primaryKey().defaultRandom(), - userId: integer('user_id').references(() => users.id).notNull(), + userId: integer('user_id').notNull(), // Jurisdictions primaryJurisdiction: text('primary_jurisdiction').notNull(), diff --git a/Cyrano/tests/routes/onboarding.test.ts b/Cyrano/tests/routes/onboarding.test.ts index caea8ccf..b4dc2a1f 100644 --- a/Cyrano/tests/routes/onboarding.test.ts +++ b/Cyrano/tests/routes/onboarding.test.ts @@ -103,9 +103,13 @@ describeIfDatabaseConfigured('Onboarding API Integration Tests', () => { }, 30000); afterAll(async () => { - if (stopServer) { - await stopServer(); - } + await new Promise((resolve) => { + if (server) { + server.close(() => resolve()); + } else { + resolve(); + } + }); }); describe('POST /api/onboarding/practice-profile', () => {