diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index 8f16e19..e1b30e3 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -5,7 +5,8 @@ import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') - const next = searchParams.get('next') ?? '/' + const nextRaw = searchParams.get('next') ?? '/' + const next = nextRaw.startsWith('/') && !nextRaw.startsWith('//') ? nextRaw : '/' if (code) { const supabase = await createClient() diff --git a/app/api/capa/[id]/route.ts b/app/api/capa/[id]/route.ts index 2cd446c..e44b053 100644 --- a/app/api/capa/[id]/route.ts +++ b/app/api/capa/[id]/route.ts @@ -24,7 +24,19 @@ export async function GET( .single() if (error || !data) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(data) + + const { data: profile } = await supabase.from('users').select('role').eq('id', user.id).single() + const role = profile?.role ?? '' + const isOwner = data.owner_user_id === user.id + const canRead = ['hse', 'admin', 'supervisor'].includes(role) || isOwner + if (!canRead) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + // Strip owner email for non-privileged roles — only name needed + const result = ['hse', 'admin'].includes(role) + ? data + : { ...data, owner: { name: (data.owner as unknown as { name: string } | null)?.name ?? '' } } + + return NextResponse.json(result) } export async function PATCH( @@ -39,9 +51,14 @@ export async function PATCH( const { data: profile } = await supabase.from('users').select('role').eq('id', user.id).single() const role = profile?.role ?? '' + const { data: capa } = await supabase.from('capa_actions').select('owner_user_id').eq('id', id).single() + const isOwner = capa?.owner_user_id === user.id + const canEdit = ['hse', 'admin'].includes(role) || isOwner + if (!canEdit) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + const body = await request.json() - // Only hse/admin can set privileged statuses — owner can only move to in_progress/pending_verification + // Only hse/admin can set privileged statuses const privilegedStatuses = ['verified', 'reopened', 'closed'] if (body.status && privilegedStatuses.includes(body.status) && !['hse', 'admin'].includes(role)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) diff --git a/app/api/cron/effectiveness-recheck/route.ts b/app/api/cron/effectiveness-recheck/route.ts index 93c81af..fb49fd1 100644 --- a/app/api/cron/effectiveness-recheck/route.ts +++ b/app/api/cron/effectiveness-recheck/route.ts @@ -1,13 +1,17 @@ export const dynamic = 'force-dynamic' +import { timingSafeEqual } from 'crypto' import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { sendEffectivenessRecheckNotifications } from '@/lib/notifications/effectiveness-recheck' export async function GET(request: NextRequest) { - const auth = request.headers.get('authorization') - const expected = `Bearer ${process.env.CRON_SECRET}` - if (!auth || auth !== expected) { + const auth = request.headers.get('authorization') ?? '' + const expected = `Bearer ${process.env.CRON_SECRET ?? ''}` + const authBuf = Buffer.from(auth, 'utf8') + const expectedBuf = Buffer.from(expected, 'utf8') + const valid = authBuf.length === expectedBuf.length && timingSafeEqual(authBuf, expectedBuf) + if (!valid) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } diff --git a/app/api/incidents/[id]/route.ts b/app/api/incidents/[id]/route.ts index 78083f3..ca2f37e 100644 --- a/app/api/incidents/[id]/route.ts +++ b/app/api/incidents/[id]/route.ts @@ -8,6 +8,10 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str const supabase = await createClient() const { data, error: authError } = await supabase.auth.getUser() if (authError || !data?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const user = data.user + + const { data: profile } = await supabase.from('users').select('role, site_id').eq('id', user.id).single() + const role = profile?.role ?? '' const { data: incident, error } = await supabase .from('incidents') @@ -28,5 +32,12 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str return NextResponse.json({ error: 'Not found' }, { status: 404 }) } + const reporter = incident.reporter as unknown as { id: string } | null + const isOwner = reporter?.id === user.id + const isSiteStaff = ['hse', 'supervisor', 'admin'].includes(role) + if (!isOwner && !isSiteStaff) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + return NextResponse.json(incident) }