-
Notifications
You must be signed in to change notification settings - Fork 743
Claude/spe m medical system 011 cu sepw2 l5 ff5k3a9x8bv t #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { auth } from "@/lib/auth"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { headers } from "next/headers"; | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: await headers(), | ||
| }); | ||
|
|
||
| if (!session?.user) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ user: session.user }); | ||
| } catch (error) { | ||
| console.error("Error fetching user:", error); | ||
| return NextResponse.json( | ||
| { error: "Failed to fetch user" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { auth } from "@/lib/auth"; | ||
| import { db } from "@/db/drizzle"; | ||
| import { forms, formCriteria, auditLogs } from "@/db/schema"; | ||
| import { eq, and } from "drizzle-orm"; | ||
| import { nanoid } from "nanoid"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { headers } from "next/headers"; | ||
| import { calculateTotalScore, classifyProfile } from "@/lib/spe-m-criteria"; | ||
|
|
||
| // POST /api/forms/[id]/finalize - Finalize form (lock for editing) | ||
| export async function POST( | ||
| request: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| try { | ||
| const session = await auth.api.getSession({ | ||
| headers: await headers(), | ||
| }); | ||
|
|
||
| if (!session?.user) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { id } = await params; | ||
|
|
||
| // Check if form exists and belongs to user | ||
| const existingForm = await db | ||
| .select() | ||
| .from(forms) | ||
| .where(and(eq(forms.id, id), eq(forms.userId, session.user.id))) | ||
| .limit(1); | ||
|
|
||
| if (existingForm.length === 0) { | ||
| return NextResponse.json({ error: "Form not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| if (existingForm[0].status === "finalized") { | ||
| return NextResponse.json( | ||
| { error: "Form is already finalized" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Get all criteria to recalculate score | ||
| const criteria = await db | ||
| .select() | ||
| .from(formCriteria) | ||
| .where(eq(formCriteria.formId, id)); | ||
|
|
||
| // Calculate final score | ||
| const criteriaData = criteria.map((c) => ({ | ||
| criterionNumber: c.criterionNumber, | ||
| data: (c.data as Record<string, string>) || {}, | ||
| })); | ||
|
|
||
| const totalScore = calculateTotalScore(criteriaData); | ||
| const profile = classifyProfile(totalScore); | ||
|
|
||
| // Update form to finalized status | ||
| await db | ||
| .update(forms) | ||
| .set({ | ||
| status: "finalized", | ||
| totalScore: totalScore.toString(), | ||
| profileClassification: profile.classification, | ||
| finalizedAt: new Date(), | ||
| updatedAt: new Date(), | ||
| }) | ||
| .where(eq(forms.id, id)); | ||
|
|
||
| // Create audit log | ||
| await db.insert(auditLogs).values({ | ||
| id: nanoid(), | ||
| userId: session.user.id, | ||
| action: "update", | ||
| entityType: "form", | ||
| entityId: id, | ||
| ipAddress: request.ip || null, | ||
| userAgent: request.headers.get("user-agent") || null, | ||
| metadata: { action: "finalized", totalScore, classification: profile.classification }, | ||
| timestamp: new Date(), | ||
| }); | ||
|
|
||
| return NextResponse.json({ | ||
| message: "Form finalized successfully", | ||
| totalScore, | ||
| profileClassification: profile, | ||
| }); | ||
|
Comment on lines
+84
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Response shape inconsistent with other endpoints. Return the classification string under profileClassification; include full profile separately if needed. - return NextResponse.json({
- message: "Form finalized successfully",
- totalScore,
- profileClassification: profile,
- });
+ return NextResponse.json({
+ message: "Form finalized successfully",
+ totalScore,
+ profileClassification: profile.classification,
+ profile,
+ });🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| console.error("Error finalizing form:", error); | ||
| return NextResponse.json( | ||
| { error: "Failed to finalize form" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,206 @@ | ||||||
| import { auth } from "@/lib/auth"; | ||||||
| import { db } from "@/db/drizzle"; | ||||||
| import { forms, formImages, auditLogs } from "@/db/schema"; | ||||||
| import { eq, and } from "drizzle-orm"; | ||||||
| import { nanoid } from "nanoid"; | ||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||
| import { headers } from "next/headers"; | ||||||
| import { uploadImage } from "@/lib/upload-image"; | ||||||
|
|
||||||
| // POST /api/forms/[id]/images - Upload image for form | ||||||
| export async function POST( | ||||||
| request: NextRequest, | ||||||
| { params }: { params: Promise<{ id: string }> } | ||||||
| ) { | ||||||
| try { | ||||||
| const session = await auth.api.getSession({ | ||||||
| headers: await headers(), | ||||||
| }); | ||||||
|
|
||||||
| if (!session?.user) { | ||||||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||||
| } | ||||||
|
|
||||||
| const { id } = await params; | ||||||
|
|
||||||
| // Check if form exists and belongs to user | ||||||
| const existingForm = await db | ||||||
| .select() | ||||||
| .from(forms) | ||||||
| .where(and(eq(forms.id, id), eq(forms.userId, session.user.id))) | ||||||
| .limit(1); | ||||||
|
|
||||||
| if (existingForm.length === 0) { | ||||||
| return NextResponse.json({ error: "Form not found" }, { status: 404 }); | ||||||
| } | ||||||
|
|
||||||
| const formData = await request.formData(); | ||||||
| const file = formData.get("file") as File; | ||||||
| const imageType = formData.get("imageType") as string; | ||||||
|
|
||||||
| if (!file || !imageType) { | ||||||
| return NextResponse.json( | ||||||
| { error: "File and image type are required" }, | ||||||
| { status: 400 } | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Validate image type | ||||||
| const validImageTypes = [ | ||||||
| "frontal", | ||||||
| "profile_right", | ||||||
| "profile_left", | ||||||
| "oblique_right", | ||||||
| "oblique_left", | ||||||
| "base", | ||||||
| ]; | ||||||
|
|
||||||
| if (!validImageTypes.includes(imageType)) { | ||||||
| return NextResponse.json( | ||||||
| { error: "Invalid image type" }, | ||||||
| { status: 400 } | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Check if image of this type already exists for this form | ||||||
| const existingImage = await db | ||||||
| .select() | ||||||
| .from(formImages) | ||||||
| .where( | ||||||
| and(eq(formImages.formId, id), eq(formImages.imageType, imageType)) | ||||||
| ) | ||||||
| .limit(1); | ||||||
|
|
||||||
| // Upload to R2 storage | ||||||
| const imageUrl = await uploadImage(file, `spe-m/${id}/${imageType}`); | ||||||
|
|
||||||
| const imageId = nanoid(); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When updating an existing image we still generate a new identifier, so the audit log’s entityId no longer matches the stored record. Reuse the existing image id so updates are audited against the correct entity. Prompt for AI agents
Suggested change
|
||||||
| const imageData = { | ||||||
| id: imageId, | ||||||
| formId: id, | ||||||
| imageType, | ||||||
| storageUrl: imageUrl, | ||||||
| thumbnailUrl: null, // TODO: Generate thumbnail | ||||||
| annotations: null, | ||||||
| metadata: { | ||||||
| fileName: file.name, | ||||||
| fileSize: file.size, | ||||||
| mimeType: file.type, | ||||||
| }, | ||||||
| uploadedAt: new Date(), | ||||||
| updatedAt: new Date(), | ||||||
| }; | ||||||
|
|
||||||
| // If image exists, update it; otherwise insert | ||||||
| if (existingImage.length > 0) { | ||||||
| await db | ||||||
| .update(formImages) | ||||||
| .set({ | ||||||
| storageUrl: imageUrl, | ||||||
| metadata: imageData.metadata, | ||||||
| updatedAt: new Date(), | ||||||
| }) | ||||||
| .where(eq(formImages.id, existingImage[0].id)); | ||||||
| } else { | ||||||
| await db.insert(formImages).values(imageData); | ||||||
| } | ||||||
|
|
||||||
| // Create audit log | ||||||
| await db.insert(auditLogs).values({ | ||||||
| id: nanoid(), | ||||||
| userId: session.user.id, | ||||||
| action: "create", | ||||||
| entityType: "image", | ||||||
| entityId: imageId, | ||||||
| ipAddress: request.ip || null, | ||||||
| userAgent: request.headers.get("user-agent") || null, | ||||||
| metadata: { formId: id, imageType }, | ||||||
| timestamp: new Date(), | ||||||
| }); | ||||||
|
|
||||||
| return NextResponse.json( | ||||||
| { image: existingImage.length > 0 ? existingImage[0] : imageData }, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The response for an updated image returns the pre-update snapshot from Prompt for AI agents |
||||||
| { status: existingImage.length > 0 ? 200 : 201 } | ||||||
| ); | ||||||
| } catch (error) { | ||||||
| console.error("Error uploading image:", error); | ||||||
| return NextResponse.json( | ||||||
| { error: "Failed to upload image" }, | ||||||
| { status: 500 } | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // PUT /api/forms/[id]/images - Update image annotations | ||||||
| export async function PUT( | ||||||
| request: NextRequest, | ||||||
| { params }: { params: Promise<{ id: string }> } | ||||||
| ) { | ||||||
| try { | ||||||
| const session = await auth.api.getSession({ | ||||||
| headers: await headers(), | ||||||
| }); | ||||||
|
|
||||||
| if (!session?.user) { | ||||||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||||
| } | ||||||
|
|
||||||
| const { id } = await params; | ||||||
| const body = await request.json(); | ||||||
| const { imageId, annotations } = body; | ||||||
|
|
||||||
| if (!imageId || !annotations) { | ||||||
| return NextResponse.json( | ||||||
| { error: "Image ID and annotations are required" }, | ||||||
| { status: 400 } | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Check if image exists and belongs to user's form | ||||||
| const existingImage = await db | ||||||
| .select({ | ||||||
| image: formImages, | ||||||
| form: forms, | ||||||
| }) | ||||||
| .from(formImages) | ||||||
| .leftJoin(forms, eq(formImages.formId, forms.id)) | ||||||
| .where( | ||||||
| and(eq(formImages.id, imageId), eq(forms.userId, session.user.id)) | ||||||
| ) | ||||||
| .limit(1); | ||||||
|
|
||||||
| if (existingImage.length === 0) { | ||||||
| return NextResponse.json({ error: "Image not found" }, { status: 404 }); | ||||||
| } | ||||||
|
|
||||||
| // Update annotations | ||||||
| await db | ||||||
| .update(formImages) | ||||||
| .set({ | ||||||
| annotations, | ||||||
| updatedAt: new Date(), | ||||||
| }) | ||||||
| .where(eq(formImages.id, imageId)); | ||||||
|
|
||||||
| // Create audit log | ||||||
| await db.insert(auditLogs).values({ | ||||||
| id: nanoid(), | ||||||
| userId: session.user.id, | ||||||
| action: "update", | ||||||
| entityType: "image", | ||||||
| entityId: imageId, | ||||||
| ipAddress: request.ip || null, | ||||||
| userAgent: request.headers.get("user-agent") || null, | ||||||
| metadata: { action: "annotations_updated" }, | ||||||
| timestamp: new Date(), | ||||||
| }); | ||||||
|
|
||||||
| return NextResponse.json({ message: "Annotations updated successfully" }); | ||||||
| } catch (error) { | ||||||
| console.error("Error updating annotations:", error); | ||||||
| return NextResponse.json( | ||||||
| { error: "Failed to update annotations" }, | ||||||
| { status: 500 } | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TOCTOU race: finalize check vs update.
Make update conditional on status != 'finalized' and verify affected rows.
Also applies to: 59-70
🤖 Prompt for AI Agents