refractor code,

This commit is contained in:
root
2026-05-21 12:35:49 -04:00
parent e74681e810
commit f009ca10c6
158 changed files with 215801 additions and 5884 deletions
+298
View File
@@ -0,0 +1,298 @@
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<string, number> = {
STARTER: 29900,
GROWTH: 59900,
PRO: 99900,
}
function signAdminToken(adminId: string) {
return jwt.sign({ sub: adminId, type: 'admin' }, process.env.JWT_SECRET!, { expiresIn: '8h' })
}
function toAuditJson<T>(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:3002/admin',
)
const resetUrl = `${adminUrl}/reset-password?token=${rawToken}`
await sendTransactionalEmail({
to: email,
subject: 'Reset your RentalDriveGo admin password',
html: `<p>Hi ${admin.firstName},</p><p><a href="${resetUrl}">Reset password</a></p><p>Expires in ${ADMIN_RESET_TTL_MINUTES} minutes.</p>`,
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
}