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>
53 lines
1.2 KiB
TypeScript
53 lines
1.2 KiB
TypeScript
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>
|
|
`,
|
|
})
|
|
}
|