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 { sendTransactionalEmail } from '../../services/notificationService' import * as presenter from './admin.presenter' import * as repo from './admin.repo' import * as billingService from './admin.billing.service' const ADMIN_RESET_TTL_MINUTES = 60 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: { q?: string; status?: string; plan?: string; page: number; pageSize: number }) { const { data, total, stats } = await billingService.listBillingAccounts(query) return presenter.presentPaginated(data, total, query.page, query.pageSize, { stats }) } export async function getCompanyInvoices(companyId: string, query: { page: number; pageSize: number }) { const detail = await billingService.getBillingAccountDetail(companyId) const start = (query.page - 1) * query.pageSize const end = start + query.pageSize const data = detail.invoices.slice(start, end) const total = detail.invoices.length return presenter.presentPaginated(data, total, query.page, query.pageSize) } export async function getInvoicePdf(invoiceId: string) { return billingService.getInvoicePdf(invoiceId) } export function getBillingAccountDetail(companyId: string) { return billingService.getBillingAccountDetail(companyId) } export function updateBillingAccount( billingAccountId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.updateBillingAccount(billingAccountId, data, adminId, ip) } export function setDunningPaused(billingAccountId: string, paused: boolean, adminId: string, ip?: string) { return billingService.setDunningPaused(billingAccountId, paused, adminId, ip) } export function createBillingInvoice( billingAccountId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.createDraftInvoice(billingAccountId, data, adminId, ip) } export function finalizeBillingInvoice(invoiceId: string, adminId: string, ip?: string) { return billingService.finalizeInvoice(invoiceId, adminId, ip) } export function payBillingInvoice( invoiceId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.payInvoice(invoiceId, data, adminId, ip) } export function retryBillingInvoicePayment( invoiceId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.retryInvoicePayment(invoiceId, data, adminId, ip) } export function voidBillingInvoice(invoiceId: string, reason: string, adminId: string, ip?: string) { return billingService.voidInvoice(invoiceId, reason, adminId, ip) } export function markBillingInvoiceUncollectible(invoiceId: string, reason: string, adminId: string, ip?: string) { return billingService.markInvoiceUncollectible(invoiceId, reason, adminId, ip) } export function issueBillingCreditNote( invoiceId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.issueCreditNote(invoiceId, data, adminId, ip) } export function issueBillingRefund( invoiceId: string, data: Parameters[1], adminId: string, ip?: string, ) { return billingService.issueRefund(invoiceId, data, adminId, ip) } 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 } export function getPricingConfigs() { return repo.listPricingConfigs() } export async function updatePricingConfigs(entries: { plan: string; billingPeriod: string; amount: number }[], adminId: string, ip?: string) { const results = await Promise.all( entries.map((e) => repo.upsertPricingConfig(e.plan, e.billingPeriod, e.amount, adminId)), ) await repo.createAuditLog({ adminUserId: adminId, action: 'UPDATE', resource: 'PricingConfig', after: toAuditJson(results), ipAddress: ip, userAgent: undefined, }) return results } export function listPlanFeatures() { return repo.listPlanFeatures() } export async function createPlanFeature(data: Parameters[0], adminId: string, ip?: string) { const feature = await repo.createPlanFeature(data) await repo.createAuditLog({ adminUserId: adminId, action: 'CREATE', resource: 'PlanFeature', after: toAuditJson(feature), ipAddress: ip, userAgent: undefined, }) return feature } export async function updatePlanFeature(id: string, data: Parameters[1], adminId: string, ip?: string) { const feature = await repo.updatePlanFeature(id, data) await repo.createAuditLog({ adminUserId: adminId, action: 'UPDATE', resource: 'PlanFeature', resourceId: id, after: toAuditJson(feature), ipAddress: ip, userAgent: undefined, }) return feature } export async function deletePlanFeature(id: string, adminId: string, ip?: string) { await repo.deletePlanFeature(id) await repo.createAuditLog({ adminUserId: adminId, action: 'DELETE', resource: 'PlanFeature', resourceId: id, ipAddress: ip, userAgent: undefined, }) } // ─── Promotions ──────────────────────────────────────────────────────────── export function listPromotions() { return repo.listPromotions() } export async function createPromotion(data: Parameters[0], adminId: string, ip?: string) { const promo = await repo.createPromotion({ ...data, createdBy: adminId }) await repo.createAuditLog({ adminUserId: adminId, action: 'CREATE', resource: 'PricingPromotion', after: toAuditJson(promo), ipAddress: ip, userAgent: undefined, }) return promo } export async function updatePromotion(id: string, data: Parameters[1], adminId: string, ip?: string) { const promo = await repo.updatePromotion(id, data) await repo.createAuditLog({ adminUserId: adminId, action: 'UPDATE', resource: 'PricingPromotion', after: toAuditJson(promo), ipAddress: ip, userAgent: undefined, }) return promo } export async function deletePromotion(id: string, adminId: string, ip?: string) { await repo.deletePromotion(id) await repo.createAuditLog({ adminUserId: adminId, action: 'DELETE', resource: 'PricingPromotion', entityId: id, ipAddress: ip, userAgent: undefined, }) }