import bcrypt from 'bcryptjs' import jwt from 'jsonwebtoken' import crypto from 'crypto' import { authenticator } from 'otplib' import qrcode from 'qrcode' import { getMarketplaceHomepageContent, saveMarketplaceHomepageContent } from '../../services/platformContentService' import { generateInvoicePdf, buildInvoiceNumber } from '../../services/invoicePdfService' import { sendTransactionalEmail } from '../../services/notificationService' import * as presenter from './admin.presenter' import * as repo from './admin.repo' const ADMIN_RESET_TTL_MINUTES = 60 const PLAN_MONTHLY_AMOUNT: Record = { STARTER: 2990, GROWTH: 3990, PRO: 99900, } function signAdminToken(adminId: string) { return jwt.sign({ sub: adminId, type: 'admin' }, process.env.JWT_SECRET!, { expiresIn: '8h' }) } function toAuditJson(value: T) { return JSON.parse(JSON.stringify(value)) } function ensureAdminBasePath(baseUrl: string) { try { const url = new URL(baseUrl) const pathname = url.pathname.replace(/\/$/, '') if (!pathname.endsWith('/admin')) url.pathname = `${pathname}/admin` return url.toString().replace(/\/$/, '') } catch { const trimmed = baseUrl.replace(/\/$/, '') return trimmed.endsWith('/admin') ? trimmed : `${trimmed}/admin` } } export async function login(email: string, password: string, totpCode?: string) { const admin = await repo.findAdminByEmail(email) if (!admin || !admin.isActive) return null const valid = await bcrypt.compare(password, admin.passwordHash) if (!valid) return null if (admin.totpEnabled) { if (!totpCode) return { totpRequired: true } as const if (!authenticator.verify({ token: totpCode, secret: admin.totpSecret! })) { return { invalidTotp: true } as const } } await repo.updateAdminLastLogin(admin.id) await repo.createAuditLog({ adminUserId: admin.id, action: 'ADMIN_LOGIN', resource: 'AdminUser', resourceId: admin.id, }) return presenter.presentAdminSession(admin, signAdminToken(admin.id)) } export async function setupTotp(adminId: string, email: string) { const secret = authenticator.generateSecret() await repo.updateAdminTotpSecret(adminId, secret) const otpauth = authenticator.keyuri(email, 'RentalDriveGo Admin', secret) const qrCode = await qrcode.toDataURL(otpauth) return { secret, qrCode } } export async function verifyTotp(adminId: string, code: string) { const admin = await repo.findAdminByIdOrThrow(adminId) if (!admin.totpSecret) return false const valid = authenticator.verify({ token: code, secret: admin.totpSecret }) if (valid) await repo.enableAdminTotp(adminId) return valid } export async function forgotPassword(email: string) { const admin = await repo.findAdminByEmail(email) if (!admin || !admin.isActive) return const rawToken = crypto.randomBytes(32).toString('hex') const expiresAt = new Date(Date.now() + ADMIN_RESET_TTL_MINUTES * 60 * 1000) await repo.setAdminPasswordReset(admin.id, rawToken, expiresAt) const adminUrl = ensureAdminBasePath( process.env.ADMIN_URL ?? process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3000/admin', ) const resetUrl = `${adminUrl}/reset-password?token=${rawToken}` await sendTransactionalEmail({ to: admin.email, subject: 'Reset your RentalDriveGo admin password', html: `

Hi ${admin.firstName},

Reset password

Expires in ${ADMIN_RESET_TTL_MINUTES} minutes.

`, text: `Hi ${admin.firstName},\n\nReset here: ${resetUrl}\n\nExpires in ${ADMIN_RESET_TTL_MINUTES} minutes.`, }).catch((err) => console.error('[AdminForgotPassword]', err?.message)) } export async function resetPassword(token: string, password: string) { const admin = await repo.findAdminByResetToken(token) if (!admin) return false await repo.updateAdminPassword(admin.id, await bcrypt.hash(password, 12)) return true } export async function listCompanies(query: { q?: string; status?: string; plan?: string; page: number; pageSize: number }) { const { data, total } = await repo.listCompaniesPage(query) return presenter.presentPaginated(data, total, query.page, query.pageSize) } export function getCompany(id: string) { return repo.getCompanyDetail(id) } export async function updateCompany(id: string, body: any, adminId: string, ip?: string) { const before = await repo.getCompanyUpdateSnapshot(id) const updated = await repo.applyCompanyUpdate(id, body, before) await repo.createAuditLog({ adminUserId: adminId, action: 'UPDATE_COMPANY', resource: 'Company', resourceId: id, companyId: id, before: toAuditJson(before), after: toAuditJson(body), ipAddress: ip, }) return updated } export async function setCompanyStatus(id: string, status: string, reason: string | undefined, adminId: string, ip?: string) { const before = await repo.getCompanyUpdateSnapshot(id) const updated = await repo.updateCompanyStatus(id, status) await repo.createAuditLog({ adminUserId: adminId, action: `SET_COMPANY_STATUS_${status}`, resource: 'Company', resourceId: id, companyId: id, before: { status: before.status }, after: { status }, note: reason, ipAddress: ip, }) return updated } export async function deleteCompany(id: string, adminId: string, ip?: string) { await repo.deleteCompany(id) await repo.createAuditLog({ adminUserId: adminId, action: 'DELETE_COMPANY', resource: 'Company', resourceId: id, ipAddress: ip, }) } export async function impersonateCompany(id: string, adminId: string, ip?: string) { const company = await repo.getCompanyForImpersonation(id) const token = jwt.sign( { sub: company.employees[0]?.id, companyId: company.id, isImpersonation: true, type: 'employee', }, process.env.JWT_SECRET!, { expiresIn: '30m' }, ) await repo.createAuditLog({ adminUserId: adminId, action: 'IMPERSONATE_COMPANY', resource: 'Company', resourceId: id, companyId: id, ipAddress: ip, }) return { token, expiresIn: 1800 } } export async function listRenters(query: { q?: string; blocked?: string; page: number; pageSize: number }) { const { data, total } = await repo.listRentersPage(query) return presenter.presentPaginated(data, total, query.page, query.pageSize) } export function setRenterActive(id: string, isActive: boolean) { return repo.updateRenterActive(id, isActive) } export function getPlatformMetrics() { return repo.getPlatformMetricCounts() } export async function getAuditLogs(query: { adminId?: string; action?: string; companyId?: string; entityId?: string; page: number; pageSize: number }) { const { data, total } = await repo.listAuditLogsPage(query) return presenter.presentPaginated(data, total, query.page, query.pageSize) } export async function listAdmins() { const admins = await repo.listAdmins() return admins.map((admin) => presenter.presentAdminUser(admin)) } export async function createAdmin(body: { email: string; firstName: string; lastName: string; role: string; password: string; permissions?: any[] }) { const admin = await repo.createAdmin({ ...body, passwordHash: await bcrypt.hash(body.password, 12), }) return presenter.presentAdminUser(admin) } export function updateAdminRole(id: string, role: string) { return repo.updateAdminRole(id, role) } export async function updateAdminPermissions(id: string, permissions: any[]) { const admin = await repo.replaceAdminPermissions(id, permissions) return presenter.presentAdminUser(admin) } export async function getBilling(query: { status?: string; plan?: string; page: number; pageSize: number }) { const [{ data, total }, activeSubscriptions, stats] = await Promise.all([ repo.listBillingPage(query), repo.listActiveSubscriptionsForMrr(), repo.getBillingStatusCounts(), ]) const mrr = activeSubscriptions.reduce((acc, subscription) => { const monthlyAmount = PLAN_MONTHLY_AMOUNT[subscription.plan] ?? 0 return acc + (subscription.billingPeriod === 'ANNUAL' ? Math.round(monthlyAmount * 10 / 12) : monthlyAmount) }, 0) return presenter.presentPaginated(data, total, query.page, query.pageSize, { stats: { mrr, ...stats }, }) } export async function getCompanyInvoices(companyId: string, query: { page: number; pageSize: number }) { const { data, total } = await repo.listCompanyInvoicesPage(companyId, query) return presenter.presentPaginated(data, total, query.page, query.pageSize) } export async function getInvoicePdf(invoiceId: string) { const invoice = await repo.getInvoicePdfRecord(invoiceId) const invoiceNumber = buildInvoiceNumber(invoice.id, invoice.createdAt) const transactionId = invoice.amanpayTransactionId ?? invoice.paypalCaptureId ?? null const pdfBuffer = await generateInvoicePdf({ invoiceNumber, issueDate: invoice.createdAt.toISOString(), dueDate: invoice.status === 'PENDING' ? invoice.createdAt.toISOString() : null, company: { name: invoice.company.name, email: invoice.company.email, phone: invoice.company.phone, address: invoice.company.address, }, subscription: { plan: invoice.subscription.plan, billingPeriod: invoice.subscription.billingPeriod, currentPeriodStart: invoice.subscription.currentPeriodStart?.toISOString(), currentPeriodEnd: invoice.subscription.currentPeriodEnd?.toISOString(), currency: invoice.subscription.currency, }, amount: invoice.amount, currency: invoice.currency, status: invoice.status, paymentProvider: invoice.paymentProvider, transactionId, paidAt: invoice.paidAt?.toISOString(), }) return { pdfBuffer, invoiceNumber } } export function getMarketplaceHomepage() { return getMarketplaceHomepageContent() } export async function updateMarketplaceHomepage(homepage: any, adminId: string, ip?: string) { const saved = await saveMarketplaceHomepageContent(homepage) await repo.createAuditLog({ adminUserId: adminId, action: 'UPDATE', resource: 'MarketplaceHomepage', after: toAuditJson(saved), ipAddress: ip, userAgent: undefined, }) return saved }