Files
ims/app/api/incidents/ai/quality-check/route.ts
T
adminandClaude Sonnet 4.6 d18d29168a feat(auth): phase 3 — replace Supabase GoTrue with bcryptjs+jose
Custom auth stack: bcryptjs password hashing (cost 10, GoTrue-compatible),
jose JWT session cookies (edge-safe, 8hr TTL), new API routes for
login/logout/reset/change-password, middleware rewritten to JWT-only
verification with no DB access. All 38 protected pages and API routes
migrated from supabase.auth.getUser() to getSession(). Supabase .from()
queries retained for Phase 4. lib/db/index.ts refactored to lazy Proxy
singleton to avoid module-level throw during Next.js build.

tsc: clean, build: clean, tests: 4/4 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 16:20:04 +08:00

104 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const dynamic = 'force-dynamic'
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { getSession } from '@/lib/auth/get-session'
import { createDeepSeekClient } from '@/lib/claude/client'
import { getApiKey } from '@/lib/settings'
export async function POST(request: NextRequest) {
const session = await getSession()
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const supabase = await createClient()
const since = new Date(Date.now() - 60_000).toISOString()
const { count: recentCount } = await supabase
.from('audit_log')
.select('id', { count: 'exact', head: true })
.eq('changed_by', session.sub)
.eq('action', 'ai_quality_check')
.gte('changed_at', since)
if ((recentCount ?? 0) > 0)
return NextResponse.json({ error: 'Rate limited — please wait 60 seconds' }, { status: 429 })
const deepseekKey = await getApiKey(supabase, 'DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
let body: { description?: string; incident_type?: string }
try {
body = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
}
if (!body.description || !body.incident_type) {
return NextResponse.json({ error: 'description and incident_type required' }, { status: 422 })
}
let res: Awaited<ReturnType<typeof client.chat.completions.create>>
try {
res = await client.chat.completions.create({
model: 'deepseek-chat',
max_tokens: 1024,
tools: [{
type: 'function',
function: {
name: 'assess_quality',
description: 'Assess HSE incident report description quality',
parameters: {
type: 'object',
properties: {
score: { type: 'number', description: '1-10 quality score' },
passes: { type: 'boolean', description: 'True when score is 6 or above' },
feedback: { type: 'string', description: 'One-sentence quality summary' },
suggestions: {
type: 'array',
items: { type: 'string' },
description: 'Up to 3 concrete suggestions to improve the description',
},
},
required: ['score', 'passes', 'feedback', 'suggestions'],
},
},
}],
tool_choice: { type: 'function', function: { name: 'assess_quality' } },
messages: [{
role: 'user',
content: `You are an HSE reporting assistant for a Malaysian 3PL warehouse. Assess this incident report description.
Incident type: ${body.incident_type}
Description: ${body.description}
Score 110 based on: specificity (location, time, persons involved), completeness (what happened + immediate actions), and clarity. Score 6 or above passes. If score is below 6, give up to 3 actionable suggestions.`,
}],
})
} catch {
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
}
const call = res.choices[0]?.message?.tool_calls?.[0]
if (!call || call.type !== 'function') return NextResponse.json({ error: 'AI assessment failed' }, { status: 500 })
let input: { score?: unknown; passes?: unknown; feedback?: unknown; suggestions?: unknown }
try { input = JSON.parse(call.function.arguments) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (
typeof input.score !== 'number' ||
typeof input.passes !== 'boolean' ||
typeof input.feedback !== 'string' ||
!Array.isArray(input.suggestions)
) {
return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 })
}
await supabase.rpc('write_audit_log', {
p_table_name: 'incidents',
p_record_id: session.sub,
p_action: 'ai_quality_check',
p_new_value: { score: input.score, passes: input.passes, model: 'deepseek-chat' } as never,
})
return NextResponse.json(input)
}