Files
carmanagement/apps/api/src/modules/auth/auth.employee.service.ts
T
2026-05-22 17:21:52 -04:00

109 lines
4.1 KiB
TypeScript

import bcrypt from 'bcryptjs'
import crypto from 'crypto'
import jwt from 'jsonwebtoken'
import { AppError } from '../../http/errors'
import { sendTransactionalEmail } from '../../services/notificationService'
import { resetPasswordEmail, type Lang } from '../../lib/emailTranslations'
import { presentEmployeeSession } from './auth.presenter'
import * as repo from './auth.employee.repo'
import { employeeLanguageSchema, employeeLoginSchema } from './auth.employee.schemas'
import type { output } from 'zod'
const RESET_TOKEN_TTL_MINUTES = 60
type EmployeeLoginInput = output<typeof employeeLoginSchema>
type EmployeeLanguageInput = output<typeof employeeLanguageSchema>
function signEmployeeToken(employeeId: string) {
return jwt.sign(
{ sub: employeeId, type: 'employee' },
process.env.JWT_SECRET!,
{ expiresIn: (process.env.JWT_EXPIRY ?? '8h') as jwt.SignOptions['expiresIn'] },
)
}
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`
}
}
export async function getMe(employeeId: string) {
const employee = await repo.findEmployeeWithCompanyById(employeeId)
if (!employee || !employee.isActive) {
throw new AppError('Employee account not found or inactive', 401, 'unauthenticated')
}
return presentEmployeeSession(employee)
}
export async function login(body: EmployeeLoginInput) {
const employee = await repo.findEmployeeWithCompanyByEmail(body.email)
if (!employee || !employee.isActive) {
throw new AppError('Invalid email or password', 401, 'invalid_credentials')
}
if (!employee.passwordHash) {
throw new AppError('This account does not have a password yet. Use the invitation or reset email to set one first.', 401, 'password_not_set')
}
const validPassword = await bcrypt.compare(body.password, employee.passwordHash)
if (!validPassword) {
throw new AppError('Invalid email or password', 401, 'invalid_credentials')
}
return presentEmployeeSession(employee, signEmployeeToken(employee.id))
}
export async function forgotPassword(email: string) {
const employee = await repo.findActiveEmployeeByEmail(email)
if (employee) {
const rawToken = crypto.randomBytes(32).toString('hex')
const expiresAt = new Date(Date.now() + RESET_TOKEN_TTL_MINUTES * 60 * 1000)
await repo.setPasswordResetToken(employee.id, rawToken, expiresAt)
const dashboardUrl = ensureDashboardBasePath(
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
)
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
const lang = (employee.preferredLanguage as Lang) ?? 'fr'
await sendTransactionalEmail({
to: employee.email,
subject: resetPasswordEmail.subject(lang),
html: resetPasswordEmail.html(resetUrl, employee.firstName, RESET_TOKEN_TTL_MINUTES, lang),
text: resetPasswordEmail.text(resetUrl, employee.firstName, RESET_TOKEN_TTL_MINUTES, lang),
}).catch((err) => {
console.error('[ForgotPassword] Email delivery failed:', err?.message)
console.error('[ForgotPassword] Provider config — resendKey present:', !!process.env.RESEND_API_KEY, '| smtpHost:', process.env.MAIL_HOST ?? 'not set')
})
}
return { message: 'If that email is registered, a reset link has been sent.' }
}
export async function updateLanguage(employeeId: string, language: EmployeeLanguageInput['language']) {
await repo.updatePreferredLanguage(employeeId, language)
return { language }
}
export async function resetPassword(token: string, password: string) {
const employee = await repo.findEmployeeByResetToken(token)
if (!employee) {
throw new AppError('Reset link is invalid or has expired.', 400, 'invalid_token')
}
await repo.resetPassword(employee.id, await bcrypt.hash(password, 12))
return { message: 'Password updated successfully. You can now sign in.' }
}