dashboard, diagnoses, expert routes now support Authorization header fallback for Android WebView Co-Authored-By: Claude <noreply@anthropic.com>
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { Elysia } from 'elysia';
|
|
import type { ReviewDiagnosisRequest } from '@zeavis/shared';
|
|
import { isDiseaseSlug } from '@zeavis/shared';
|
|
import { and, desc, eq } from 'drizzle-orm';
|
|
import { createDbClient } from '../db/client';
|
|
import { diagnoses, expertReviews } from '../db/schema';
|
|
import { badRequest, forbidden, notFound, serviceUnavailable, unauthorized } from '../lib/http-errors';
|
|
import { getCurrentUser } from '../lib/auth';
|
|
import { loadDiagnosisRecord } from './diagnoses';
|
|
|
|
type DiagnosisRecord = Awaited<ReturnType<typeof loadDiagnosisRecord>>;
|
|
|
|
function isDiagnosisRecordOrNull(record: unknown): record is DiagnosisRecord {
|
|
return record !== null;
|
|
}
|
|
|
|
export const expertRoutes = new Elysia({ prefix: '/api/v1/expert' })
|
|
.get('/reviews', async ({ request }) => {
|
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
|
if (!user) return unauthorized('Authentication required');
|
|
if (user.role !== 'expert') return forbidden('Expert role required');
|
|
|
|
try {
|
|
const db = createDbClient();
|
|
const rows = await db
|
|
.select({ id: diagnoses.id })
|
|
.from(diagnoses)
|
|
.where(eq(diagnoses.status, 'needs_review'))
|
|
.orderBy(desc(diagnoses.createdAt))
|
|
.limit(50);
|
|
|
|
const records = await Promise.all(rows.map((row) => loadDiagnosisRecord(row.id, null, true)));
|
|
return records.filter(isDiagnosisRecordOrNull);
|
|
} catch (error) {
|
|
return serviceUnavailable('Database unavailable');
|
|
}
|
|
})
|
|
.post('/reviews/:diagnosisId', async ({ params, body, request }) => {
|
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
|
if (!user) return unauthorized('Authentication required');
|
|
if (user.role !== 'expert') return forbidden('Expert role required');
|
|
|
|
const req = body as Partial<ReviewDiagnosisRequest> | undefined;
|
|
if (!req || (req.verdict !== 'verified' && req.verdict !== 'corrected')) {
|
|
return badRequest('Verdict must be verified or corrected');
|
|
}
|
|
|
|
const notes = typeof req.notes === 'string' ? req.notes.trim() : '';
|
|
if (!notes) return badRequest('Review notes are required');
|
|
|
|
if (req.verdict === 'corrected') {
|
|
if (!req.correctedDiseaseSlug || !isDiseaseSlug(req.correctedDiseaseSlug)) {
|
|
return badRequest('Corrected disease slug is required for corrected reviews');
|
|
}
|
|
}
|
|
|
|
try {
|
|
const db = createDbClient();
|
|
const existing = await db
|
|
.select()
|
|
.from(diagnoses)
|
|
.where(eq(diagnoses.id, params.diagnosisId))
|
|
.limit(1);
|
|
|
|
const diagnosis = existing[0];
|
|
if (!diagnosis) return notFound('Diagnosis not found');
|
|
|
|
const verdict = req.verdict as 'verified' | 'corrected';
|
|
const correctedSlug = verdict === 'corrected' ? req.correctedDiseaseSlug : null;
|
|
|
|
// 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) {
|
|
throw new Error('DIAGNOSIS_NOT_PENDING_REVIEW');
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
});
|