Files
carmanagement/apps/api/src/modules/auth/auth.account.service.ts
T
root f22e0d45e1
Build & Deploy / Build & Push Docker Image (push) Successful in 7m57s
Test / Type Check (all packages) (push) Successful in 4m27s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 3m2s
Test / Homepage Unit Tests (push) Successful in 4m3s
Test / Storefront Unit Tests (push) Successful in 3m32s
Test / Admin Unit Tests (push) Successful in 3m27s
Test / Dashboard Unit Tests (push) Successful in 3m3s
Test / API Integration Tests (push) Failing after 3m53s
fix subscription page
2026-06-29 23:15:55 -04:00

137 lines
5.8 KiB
TypeScript

import bcrypt from 'bcryptjs'
import crypto from 'crypto'
import { AppError } from '../../http/errors'
import { prisma } from '../../lib/prisma'
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, 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)) {
throw new AppError('An account with this email already exists', 409, 'email_taken')
}
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()
const trialEndAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
const company = await tx.company.create({
data: {
name: '',
slug,
email: body.email,
address: {},
status: 'TRIALING',
},
})
await tx.brandSettings.create({
data: {
companyId: company.id,
displayName: body.email.split('@')[0] || 'My Workspace',
subdomain: slug,
defaultLocale: body.preferredLanguage,
defaultCurrency: 'MAD',
},
})
await tx.subscription.create({
data: {
companyId: company.id,
plan: 'STARTER',
billingPeriod: 'MONTHLY',
currency: 'MAD',
status: 'TRIALING',
trialStartAt: now,
trialEndAt,
currentPeriodStart: now,
currentPeriodEnd: trialEndAt,
},
})
const employee = await tx.employee.create({
data: {
companyId: company.id,
clerkUserId: `local_owner_${company.id}`,
firstName: '',
lastName: '',
email: body.email,
passwordHash,
emailVerificationToken: verificationToken,
role: 'OWNER',
preferredLanguage: body.preferredLanguage,
isActive: true,
},
include: { company: true },
})
return { employee, company }
})
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<'en' | 'fr' | 'ar', string> = {
en: 'Verify your email — RentalDriveGo',
fr: 'Vérifiez votre email — RentalDriveGo',
ar: 'تحقق من بريدك الإلكتروني — RentalDriveGo',
}
const emailBodies: Record<'en' | 'fr' | 'ar', string> = {
en: `<p>Welcome to RentalDriveGo!</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 RentalDriveGo !</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>مرحباً بك في RentalDriveGo!</p><p>انقر على الرابط أدناه للتحقق من بريدك الإلكتروني وتفعيل حسابك:</p><p><a href="${verifyUrl}">تحقق من البريد الإلكتروني</a></p><p>إذا لم تقم بإنشاء هذا الحساب، يمكنك تجاهل هذا البريد الإلكتروني.</p>`,
}
const emailTexts: Record<'en' | 'fr' | 'ar', string> = {
en: `Welcome to RentalDriveGo!\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 RentalDriveGo !\n\nVérifiez votre email en visitant :\n${verifyUrl}\n\nSi vous n'avez pas créé ce compte, ignorez cet email.`,
ar: `مرحباً بك في RentalDriveGo!\n\nتحقق من بريدك الإلكتروني بزيارة:\n${verifyUrl}\n\nإذا لم تقم بإنشاء هذا الحساب، يمكنك تجاهل هذا البريد الإلكتروني.`,
}
const lang: 'en' | 'fr' | 'ar' =
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 ?? String(err))
console.error('[AccountStart] SMTP config — host:', process.env.MAIL_HOST ?? 'not set', '| port:', process.env.MAIL_PORT ?? 'not set', '| user:', process.env.MAIL_USERNAME ? '***' : 'not set', '| pass:', process.env.MAIL_PASSWORD ? '***' : 'not set')
console.error('[AccountStart] Resend config — apiKey:', process.env.RESEND_API_KEY ? (process.env.RESEND_API_KEY.startsWith('re_') ? 'valid' : 'placeholder') : 'not set')
})
return {
message: 'Account created. Please check your email to verify your address before signing in.',
email: body.email,
}
}