Files
ims/app/api/incidents/ai/quality-check/route.ts
T
adminandClaude Sonnet 4.6 16dd62df11 fix: P1 API security hardening — rate limits, auth guards, duplicate prevention
- verify/route.ts: setDate → setUTCDate to avoid timezone off-by-one on recheck date
- triage-suggest, rca-draft, quality-check: 60s per-user rate limit via audit_log
- quality-check: add write_audit_log (was missing, CLAUDE.md violation)
- investigation POST: 409 if investigation already exists for incident
- incidents POST: 60s per-user rate limit via audit_log
- addenda GET: restrict to hse/admin/supervisor roles
- dashboard/stats GET: restrict to hse/admin/management roles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 20:47:32 +08:00

104 lines
3.8 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 { createAnthropicClient } from '@/lib/claude/client'
import { getApiKey } from '@/lib/settings'
export async function POST(request: NextRequest) {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
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', user.id)
.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 anthropicKey = await getApiKey(supabase, 'ANTHROPIC_API_KEY')
const anthropic = createAnthropicClient(anthropicKey)
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 message: Awaited<ReturnType<typeof anthropic.messages.create>>
try {
message = await anthropic.messages.create({
model: 'claude-opus-4-8',
thinking: { type: 'adaptive' },
max_tokens: 1024,
tools: [{
name: 'assess_quality',
description: 'Assess HSE incident report description quality',
input_schema: {
type: 'object' as const,
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: 'tool', 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 toolBlock = message.content.find(b => b.type === 'tool_use')
if (!toolBlock || toolBlock.type !== 'tool_use') {
return NextResponse.json({ error: 'AI assessment failed' }, { status: 500 })
}
const input = toolBlock.input as {
score?: unknown
passes?: unknown
feedback?: unknown
suggestions?: unknown
}
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: user.id,
p_action: 'ai_quality_check',
p_new_value: { score: input.score, passes: input.passes, model: 'claude-opus-4-8' } as never,
})
return NextResponse.json(input)
}