From 0e479b648fa6ec03ca6cb7342cfa473475ba094b Mon Sep 17 00:00:00 2001 From: weeihan Date: Wed, 22 Jul 2026 19:43:38 +0800 Subject: [PATCH] fix(auth,capa): restore auth callback, fix CAPA status update - auth callback: remove debug redirect, handle both code (PKCE) and token_hash+type (recovery/magic link) flows correctly - capa PATCH: use admin client to bypass RLS for status updates Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01WMymkhHZiaYZtUeH9MEHZQ --- .superpowers/sdd/progress.md | 52 ++++++++++++++++++ .../hse/incidents/[id]/capa/new/page.tsx | 15 +---- app/api/auth/callback/route.ts | 14 +++-- app/api/capa/[id]/route.ts | 4 +- app/api/capa/route.ts | 4 +- components/capa/capa-form.tsx | 13 ++++- components/capa/capa-owner-actions.tsx | 7 ++- docs/~$D_HSE_IMS_Formatted.docx | Bin 162 -> 0 bytes 8 files changed, 83 insertions(+), 26 deletions(-) delete mode 100644 docs/~$D_HSE_IMS_Formatted.docx diff --git a/.superpowers/sdd/progress.md b/.superpowers/sdd/progress.md index 0598b7b..a22fcaa 100644 --- a/.superpowers/sdd/progress.md +++ b/.superpowers/sdd/progress.md @@ -112,3 +112,55 @@ Branch: phase-5-6 - [x] Drive-by: fixed 9 pre-existing missing /ims basePath prefixes Verification: 112 tests passing (23 files), tsc clean, next build clean. + +--- + +# Bugfix SDD Progress Ledger + +Plan: docs/superpowers/plans/2026-07-17-bugfixes-invite-capa-drugtest.md +Started: 2026-07-17 +Base commit: e682162 + +## Tasks + +- [x] Task 1: Fix invite redirect URL — commit a3ce59f, review clean +- [x] Task 2: CAPA Assigned To — commit e687510, review clean +- [x] Task 3: Split alcohol/urine test — commits e30d583..ac2f072, review clean (2 minor: as never cast in supervisor page; cosmetic single-test grid) + +## Final Review (2026-07-17) +Verdict: Approved — ready to merge +Minor findings (non-blocking): +- alcohol_test_result missing CHECK constraint (urine_test_result has it) — asymmetric, follow-up migration +- PATCH audit_log only on complete=true, not on every field save — pre-existing gap +- supervisor page `as never` cast — already logged +- cosmetic single-test grid — already logged + +# Remove Invite-by-Email SDD Progress Ledger + +Plan: docs/superpowers/plans/2026-07-18-remove-invite-by-email.md +Started: 2026-07-18 +Base commit: ac2f072 + +## Tasks + +- [x] Task 1: Strip invite mode from UI + API — commit 33d4dd4, review clean (minor: audit log skipped on DELETE when target row null; unreachable ?? fallback in DELETE) +- [x] Task 2: Delete invite-callback page — no commit needed (files were untracked), review clean + +## Final Review (2026-07-18) +Verdict: Approved after fix +- Important fixed: orphaned auth user on profile update failure — commit d5803da +- Minor (non-blocking): confirmDeleteId cleared before error check in deleteUser; no client-side guard disabling Delete for own account row + +# Forgot Password SDD Progress Ledger + +Plan: docs/superpowers/plans/2026-07-21-forgot-password.md +Started: 2026-07-21 +Base commit: 3a5daaa + +## Tasks + +- [x] Task 1: Add "Forgot password?" link to login form (commit 4d7ab5d, review clean) +- [x] Task 2: Create forgot-password page (commit 211117e, review clean — reviewer finding 1 false positive: /reset-password page created by Task 3; finding 3 false positive: disabled={loading} already on button; finding 2 minor: appUrl fallback non-blocking, NEXT_PUBLIC_APP_URL set in .env.production) +- [x] Task 3: Create reset-password page + login success banner (commit fa88d62, review clean) +- [x] Task 4: Deploy to VPS (commit fa88d62 + 4fbab33, deployed) +- [x] Final review fixes: middleware isPublicRoute, Link basePath, session guard (commit 4fbab33, re-review approved) diff --git a/app/(protected)/hse/incidents/[id]/capa/new/page.tsx b/app/(protected)/hse/incidents/[id]/capa/new/page.tsx index 7f7c09a..8d9f139 100644 --- a/app/(protected)/hse/incidents/[id]/capa/new/page.tsx +++ b/app/(protected)/hse/incidents/[id]/capa/new/page.tsx @@ -1,9 +1,6 @@ -export const dynamic = 'force-dynamic' - import { notFound, redirect } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/server' -import { createAdminClient } from '@/lib/supabase/admin' import { CapaForm } from '@/components/capa/capa-form' interface Props { @@ -24,13 +21,6 @@ export default async function NewCapaPage({ params }: Props) { .from('incidents').select('id, reference_no, status').eq('id', id).single() if (!incident) notFound() - const admin = createAdminClient() - const { data: users } = await admin - .from('users') - .select('id, name, department') - .eq('active', true) - .order('name') - return (
@@ -40,10 +30,7 @@ export default async function NewCapaPage({ params }: Props) {

{(incident as { reference_no: string | null }).reference_no ?? id}

- } - /> +
) } diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index c1d5816..edfe745 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -1,20 +1,24 @@ -// app/api/auth/callback/route.ts import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' +import type { EmailOtpType } from '@supabase/supabase-js' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const code = searchParams.get('code') + const token_hash = searchParams.get('token_hash') + const type = searchParams.get('type') as EmailOtpType | null const nextRaw = searchParams.get('next') ?? '/' const next = nextRaw.startsWith('/') && !nextRaw.startsWith('//') ? nextRaw : '/' const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? '').replace(/\/$/, '') + const supabase = await createClient() + if (code) { - const supabase = await createClient() const { error } = await supabase.auth.exchangeCodeForSession(code) - if (!error) { - return NextResponse.redirect(`${appUrl}${next}`) - } + if (!error) return NextResponse.redirect(`${appUrl}${next}`) + } else if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ token_hash, type }) + if (!error) return NextResponse.redirect(`${appUrl}${next}`) } return NextResponse.redirect(`${appUrl}/login?error=auth_callback_failed`) diff --git a/app/api/capa/[id]/route.ts b/app/api/capa/[id]/route.ts index e44b053..21e6029 100644 --- a/app/api/capa/[id]/route.ts +++ b/app/api/capa/[id]/route.ts @@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { createAdminClient } from '@/lib/supabase/admin' export async function GET( _: NextRequest, @@ -74,7 +75,8 @@ export async function PATCH( update.completed_at = new Date().toISOString() } - const { error } = await supabase + const admin = createAdminClient() + const { error } = await admin .from('capa_actions') .update(update) .eq('id', id) diff --git a/app/api/capa/route.ts b/app/api/capa/route.ts index ef7d2a5..b6f98ba 100644 --- a/app/api/capa/route.ts +++ b/app/api/capa/route.ts @@ -45,7 +45,7 @@ export async function POST(request: NextRequest) { const body = await request.json() const { incident_id, description, owner_user_id, department, due_date, priority, root_cause_ref } = body - if (!incident_id || !description || !owner_user_id || !department || !due_date) + if (!incident_id || !description || !owner_user_id || !due_date) return NextResponse.json({ error: 'Missing required fields' }, { status: 422 }) const { data: capa, error } = await supabase @@ -54,7 +54,7 @@ export async function POST(request: NextRequest) { incident_id, description, owner_user_id, - department, + department: department || '', due_date, priority: priority ?? 'med', root_cause_ref: root_cause_ref ?? null, diff --git a/components/capa/capa-form.tsx b/components/capa/capa-form.tsx index 1fc8ec5..c1426bc 100644 --- a/components/capa/capa-form.tsx +++ b/components/capa/capa-form.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' interface User { @@ -11,11 +11,11 @@ interface User { interface Props { incidentId: string - users: User[] } -export function CapaForm({ incidentId, users }: Props) { +export function CapaForm({ incidentId }: Props) { const router = useRouter() + const [users, setUsers] = useState([]) const [description, setDescription] = useState('') const [ownerId, setOwnerId] = useState('') const [department, setDepartment] = useState('') @@ -25,6 +25,13 @@ export function CapaForm({ incidentId, users }: Props) { const [saving, setSaving] = useState(false) const [error, setError] = useState(null) + useEffect(() => { + fetch('/ims/api/users') + .then(r => r.json()) + .then(data => { if (Array.isArray(data)) setUsers(data) }) + .catch(() => {}) + }, []) + function handleOwnerChange(id: string) { setOwnerId(id) const u = users.find(u => u.id === id) diff --git a/components/capa/capa-owner-actions.tsx b/components/capa/capa-owner-actions.tsx index 6f4433f..a36bfb2 100644 --- a/components/capa/capa-owner-actions.tsx +++ b/components/capa/capa-owner-actions.tsx @@ -15,12 +15,17 @@ export function CapaOwnerActions({ capaId, currentStatus }: Props) { async function updateStatus(status: string) { setLoading(true) try { - await fetch(`/ims/api/capa/${capaId}`, { + const res = await fetch(`/ims/api/capa/${capaId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }) + if (!res.ok) { + alert('Update failed. Please try again.') + return + } router.refresh() + setTimeout(() => window.location.reload(), 300) } finally { setLoading(false) } diff --git a/docs/~$D_HSE_IMS_Formatted.docx b/docs/~$D_HSE_IMS_Formatted.docx deleted file mode 100644 index dba0200b70b543c1e512c3272c4df1fb0cf49d47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmWgj%}g%JFV0UZQSeVo%S=vH2rW)6VjuuS8GIQs8Il=_81fm4fjEt!gh7G9A4sQx z#Z!U2P@qgIPz3|SR$FEunG*DYgAqt;=zR_Y(I2Kj#FeH(Xp6}ZS^-M$35KW#04K8@ A#sB~S