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>
This commit is contained in:
2026-07-23 16:20:04 +08:00
co-authored by Claude Sonnet 4.6
parent a95273b182
commit d18d29168a
67 changed files with 966 additions and 591 deletions
+52
View File
@@ -0,0 +1,52 @@
interface SendEmailParams {
to: string
subject: string
html: string
}
async function sendEmail({ to, subject, html }: SendEmailParams): Promise<void> {
const apiKey = process.env.BREVO_API_KEY
if (!apiKey) throw new Error('BREVO_API_KEY not configured')
const from = process.env.BREVO_FROM_EMAIL ?? 'noreply@setia.com.my'
const res = await fetch('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: {
'api-key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
sender: { email: from },
to: [{ email: to }],
subject,
htmlContent: html,
}),
})
if (!res.ok) {
const text = await res.text()
throw new Error(`Brevo API error ${res.status}: ${text}`)
}
}
export async function sendPasswordResetEmail({
to,
name,
resetLink,
}: {
to: string
name: string
resetLink: string
}): Promise<void> {
await sendEmail({
to,
subject: 'IMS — Reset your password',
html: `
<p>Hi ${name},</p>
<p>Click the link below to reset your IMS password. The link expires in 1 hour.</p>
<p><a href="${resetLink}">${resetLink}</a></p>
<p>If you did not request a password reset, ignore this email.</p>
`,
})
}