fix: harden production auth and review flow

- Add environment-aware CORS and secure cookie support:
  * Add secureCookies config to env.ts based on SECURE_COOKIES env var or https detection
  * Integrate @elysiajs/cors with credentials and origin configuration
  * Update cookie helpers to use SameSite=None; Secure in production
- Wrap expert review update and insert in database transaction for atomicity:
  * Ensures diagnosis status update and review insert succeed together
  * Rolls back both operations if either fails
  * Preserves behavior: only update if status is needs_review, return badRequest if no row updated

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Asep Haryana Saputra
2026-05-22 17:32:57 +00:00
co-authored by Claude Opus 4.7
parent 4478d51aae
commit c623de9959
6 changed files with 50 additions and 25 deletions
+5 -1
View File
@@ -7,6 +7,9 @@ const googleOAuthEnabled = Boolean(
Bun.env.GOOGLE_CLIENT_ID && Bun.env.GOOGLE_CLIENT_SECRET && Bun.env.GOOGLE_REDIRECT_URI,
);
const webAppUrl = Bun.env.WEB_APP_URL ?? 'http://localhost:5173';
const secureCookies = Bun.env.SECURE_COOKIES === 'true' || webAppUrl.startsWith('https://');
export const env = {
port: Number(Bun.env.API_PORT ?? 3000),
databaseUrl: Bun.env.DATABASE_URL,
@@ -18,7 +21,8 @@ export const env = {
googleClientId: Bun.env.GOOGLE_CLIENT_ID,
googleClientSecret: Bun.env.GOOGLE_CLIENT_SECRET,
googleRedirectUri: Bun.env.GOOGLE_REDIRECT_URI,
webAppUrl: Bun.env.WEB_APP_URL ?? 'http://localhost:5173',
webAppUrl,
secureCookies,
};
export function assertRequiredEnv() {
+5
View File
@@ -1,4 +1,5 @@
import { Elysia } from 'elysia';
import cors from '@elysiajs/cors';
import { env, assertRequiredEnv } from './config/env';
import { healthRoutes } from './routes/health';
import { statusRoutes } from './routes/status';
@@ -12,6 +13,10 @@ import { expertRoutes } from './routes/expert';
assertRequiredEnv();
const app = new Elysia()
.use(cors({
origin: env.webAppUrl,
credentials: true,
}))
.use(healthRoutes)
.use(statusRoutes)
.use(authRoutes)
+4 -2
View File
@@ -30,11 +30,13 @@ function hashToken(token: string) {
export function createSessionCookie(token: string) {
const maxAge = 60 * 60 * 24 * 30;
return `${sessionCookieName}=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${maxAge}`;
const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax';
return `${sessionCookieName}=${token}; HttpOnly; Path=/; ${sameSite}; Max-Age=${maxAge}`;
}
export function clearSessionCookie() {
return `${sessionCookieName}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`;
const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax';
return `${sessionCookieName}=; HttpOnly; Path=/; ${sameSite}; Max-Age=0`;
}
export function readSessionToken(cookieHeader: string | null | undefined) {
+31 -21
View File
@@ -65,34 +65,44 @@ export const expertRoutes = new Elysia({ prefix: '/api/v1/expert' })
const diagnosis = existing[0];
if (!diagnosis) return notFound('Diagnosis not found');
const correctedSlug = req.verdict === 'corrected' ? req.correctedDiseaseSlug : null;
const verdict = req.verdict as 'verified' | 'corrected';
const correctedSlug = verdict === 'corrected' ? req.correctedDiseaseSlug : null;
// Perform conditional update first to determine if this review should proceed
const updated = await db
.update(diagnoses)
.set({
status: req.verdict === 'corrected' ? 'expert_corrected' : 'expert_verified',
predictedDiseaseSlug: req.verdict === 'corrected' ? correctedSlug : diagnosis.predictedDiseaseSlug,
updatedAt: new Date(),
})
.where(and(eq(diagnoses.id, diagnosis.id), eq(diagnoses.status, 'needs_review')))
.returning();
// Wrap update and insert in a transaction for atomicity
const result = await db.transaction(async (tx) => {
// Perform conditional update first to determine if this review should proceed
const updated = await tx
.update(diagnoses)
.set({
status: verdict === 'corrected' ? 'expert_corrected' : 'expert_verified',
predictedDiseaseSlug: verdict === 'corrected' ? correctedSlug : diagnosis.predictedDiseaseSlug,
updatedAt: new Date(),
})
.where(and(eq(diagnoses.id, diagnosis.id), eq(diagnoses.status, 'needs_review')))
.returning();
if (updated.length === 0) {
return badRequest('Diagnosis is not pending review');
}
if (updated.length === 0) {
throw new Error('DIAGNOSIS_NOT_PENDING_REVIEW');
}
// Only insert expert review after successful update
await db.insert(expertReviews).values({
diagnosisId: diagnosis.id,
expertId: user.id,
verdict: req.verdict,
correctedDiseaseSlug: correctedSlug,
notes,
// Only insert expert review after successful update
const reviewValues = {
diagnosisId: diagnosis.id,
expertId: user.id,
verdict,
correctedDiseaseSlug: correctedSlug,
notes,
};
await tx.insert(expertReviews).values(reviewValues);
return updated[0];
});
return await loadDiagnosisRecord(diagnosis.id, null, true);
} catch (error) {
if (error instanceof Error && error.message === 'DIAGNOSIS_NOT_PENDING_REVIEW') {
return badRequest('Diagnosis is not pending review');
}
return serviceUnavailable('Database unavailable');
}
});