feat: add employee email verification on signup
Build & Deploy / Build & Push Docker Image (push) Failing after 41s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m1s
Test / Marketplace Unit Tests (push) Successful in 9m36s
Test / Admin Unit Tests (push) Successful in 9m38s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Has been cancelled
Build & Deploy / Build & Push Docker Image (push) Failing after 41s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m1s
Test / Marketplace Unit Tests (push) Successful in 9m36s
Test / Admin Unit Tests (push) Successful in 9m38s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Has been cancelled
- Signup sends verification email instead of auto-login; login blocks unverified emails - New endpoints: POST /verify-email and POST /resend-verification - New verify-email page in dashboard with token-based email confirmation - DB migration adds emailVerified and emailVerificationToken fields to Employee - Update sign-in/sign-up/reset-password flows to handle verification states - Minor UI polish across dashboard and marketplace components - Refactor marketplace-homepage types
This commit is contained in:
@@ -1,17 +1,30 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import crypto from 'crypto'
|
||||
import { AppError } from '../../http/errors'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { signActorToken } from '../../security/tokens'
|
||||
import { presentEmployeeSession } from './auth.presenter'
|
||||
import { sendTransactionalEmail } from '../../services/notificationService'
|
||||
import * as repo from './auth.company.repo'
|
||||
import type { output } from 'zod'
|
||||
import type { accountStartSchema } from './auth.account.schemas'
|
||||
|
||||
type AccountStartInput = output<typeof accountStartSchema>
|
||||
|
||||
function ensureDashboardBasePath(baseUrl: string) {
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
const pathname = url.pathname.replace(/\/$/, '')
|
||||
if (!pathname.endsWith('/dashboard')) url.pathname = `${pathname}/dashboard`
|
||||
return url.toString().replace(/\/$/, '')
|
||||
} catch {
|
||||
const trimmed = baseUrl.replace(/\/$/, '')
|
||||
return trimmed.endsWith('/dashboard') ? trimmed : `${trimmed}/dashboard`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal account creation — Step 0 of progressive signup.
|
||||
* Creates a DRAFT company + OWNER employee, logs the user in immediately.
|
||||
* Creates a DRAFT company + OWNER employee, sends a verification email.
|
||||
* The user must verify their email before they can log in.
|
||||
*/
|
||||
export async function startAccount(body: AccountStartInput) {
|
||||
if (await repo.findEmployeeByEmail(body.email)) {
|
||||
@@ -20,6 +33,7 @@ export async function startAccount(body: AccountStartInput) {
|
||||
|
||||
const slug = `company-${Date.now().toString(36)}`
|
||||
const passwordHash = await bcrypt.hash(body.password, 12)
|
||||
const verificationToken = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
const result = await prisma.$transaction(async (tx: any) => {
|
||||
const now = new Date()
|
||||
@@ -67,6 +81,7 @@ export async function startAccount(body: AccountStartInput) {
|
||||
lastName: '',
|
||||
email: body.email,
|
||||
passwordHash,
|
||||
emailVerificationToken: verificationToken,
|
||||
role: 'OWNER',
|
||||
preferredLanguage: body.preferredLanguage,
|
||||
isActive: true,
|
||||
@@ -74,12 +89,43 @@ export async function startAccount(body: AccountStartInput) {
|
||||
include: { company: true },
|
||||
})
|
||||
|
||||
return employee
|
||||
return { employee, company }
|
||||
})
|
||||
|
||||
const token = signActorToken(result.id, 'employee', {
|
||||
expiresIn: (process.env.JWT_EXPIRY ?? '8h') as any,
|
||||
const dashboardUrl = ensureDashboardBasePath(
|
||||
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||
)
|
||||
const verifyUrl = `${dashboardUrl}/verify-email?token=${encodeURIComponent(verificationToken)}`
|
||||
|
||||
const emailSubjects: Record<string, string> = {
|
||||
en: 'Verify your email — FleetOS',
|
||||
fr: 'Vérifiez votre email — FleetOS',
|
||||
ar: 'تحقق من بريدك الإلكتروني — FleetOS',
|
||||
}
|
||||
const emailBodies: Record<string, string> = {
|
||||
en: `<p>Welcome to FleetOS!</p><p>Click the link below to verify your email and activate your account:</p><p><a href="${verifyUrl}">Verify Email</a></p><p>If you did not create this account, you can safely ignore this email.</p>`,
|
||||
fr: `<p>Bienvenue sur FleetOS !</p><p>Cliquez sur le lien ci-dessous pour vérifier votre email et activer votre compte :</p><p><a href="${verifyUrl}">Vérifier l'email</a></p><p>Si vous n'avez pas créé ce compte, ignorez cet email.</p>`,
|
||||
ar: `<p>مرحباً بك في FleetOS!</p><p>انقر على الرابط أدناه للتحقق من بريدك الإلكتروني وتفعيل حسابك:</p><p><a href="${verifyUrl}">تحقق من البريد الإلكتروني</a></p><p>إذا لم تقم بإنشاء هذا الحساب، يمكنك تجاهل هذا البريد الإلكتروني.</p>`,
|
||||
}
|
||||
const emailTexts: Record<string, string> = {
|
||||
en: `Welcome to FleetOS!\n\nVerify your email by visiting:\n${verifyUrl}\n\nIf you did not create this account, you can safely ignore this email.`,
|
||||
fr: `Bienvenue sur FleetOS !\n\nVérifiez votre email en visitant :\n${verifyUrl}\n\nSi vous n'avez pas créé ce compte, ignorez cet email.`,
|
||||
ar: `مرحباً بك في FleetOS!\n\nتحقق من بريدك الإلكتروني بزيارة:\n${verifyUrl}\n\nإذا لم تقم بإنشاء هذا الحساب، يمكنك تجاهل هذا البريد الإلكتروني.`,
|
||||
}
|
||||
|
||||
const lang = (body.preferredLanguage === 'ar' || body.preferredLanguage === 'fr') ? body.preferredLanguage : 'en'
|
||||
|
||||
await sendTransactionalEmail({
|
||||
to: body.email,
|
||||
subject: emailSubjects[lang],
|
||||
html: emailBodies[lang],
|
||||
text: emailTexts[lang],
|
||||
}).catch((err) => {
|
||||
console.error('[AccountStart] Verification email delivery failed:', err?.message)
|
||||
})
|
||||
|
||||
return { token, ...presentEmployeeSession(result as any, token) }
|
||||
return {
|
||||
message: 'Account created. Please check your email to verify your address before signing in.',
|
||||
email: body.email,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user