494 lines
20 KiB
JavaScript
494 lines
20 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.login = login;
|
|
exports.setupTotp = setupTotp;
|
|
exports.verifyTotp = verifyTotp;
|
|
exports.regenerateRecoveryCodes = regenerateRecoveryCodes;
|
|
exports.forgotPassword = forgotPassword;
|
|
exports.resetPassword = resetPassword;
|
|
exports.listCompanies = listCompanies;
|
|
exports.getCompany = getCompany;
|
|
exports.updateCompany = updateCompany;
|
|
exports.setCompanyStatus = setCompanyStatus;
|
|
exports.deleteCompany = deleteCompany;
|
|
exports.impersonateCompany = impersonateCompany;
|
|
exports.listRenters = listRenters;
|
|
exports.setRenterActive = setRenterActive;
|
|
exports.getPlatformMetrics = getPlatformMetrics;
|
|
exports.listNotifications = listNotifications;
|
|
exports.getAuditLogs = getAuditLogs;
|
|
exports.listAdmins = listAdmins;
|
|
exports.createAdmin = createAdmin;
|
|
exports.updateAdmin = updateAdmin;
|
|
exports.updateAdminRole = updateAdminRole;
|
|
exports.updateAdminPermissions = updateAdminPermissions;
|
|
exports.getBilling = getBilling;
|
|
exports.getCompanyInvoices = getCompanyInvoices;
|
|
exports.getInvoicePdf = getInvoicePdf;
|
|
exports.getBillingAccountDetail = getBillingAccountDetail;
|
|
exports.updateBillingAccount = updateBillingAccount;
|
|
exports.setDunningPaused = setDunningPaused;
|
|
exports.createBillingInvoice = createBillingInvoice;
|
|
exports.finalizeBillingInvoice = finalizeBillingInvoice;
|
|
exports.payBillingInvoice = payBillingInvoice;
|
|
exports.retryBillingInvoicePayment = retryBillingInvoicePayment;
|
|
exports.voidBillingInvoice = voidBillingInvoice;
|
|
exports.markBillingInvoiceUncollectible = markBillingInvoiceUncollectible;
|
|
exports.issueBillingCreditNote = issueBillingCreditNote;
|
|
exports.issueBillingRefund = issueBillingRefund;
|
|
exports.getMarketplaceHomepage = getMarketplaceHomepage;
|
|
exports.updateMarketplaceHomepage = updateMarketplaceHomepage;
|
|
exports.getPricingConfigs = getPricingConfigs;
|
|
exports.updatePricingConfigs = updatePricingConfigs;
|
|
exports.listPlanFeatures = listPlanFeatures;
|
|
exports.createPlanFeature = createPlanFeature;
|
|
exports.updatePlanFeature = updatePlanFeature;
|
|
exports.deletePlanFeature = deletePlanFeature;
|
|
exports.listPromotions = listPromotions;
|
|
exports.createPromotion = createPromotion;
|
|
exports.updatePromotion = updatePromotion;
|
|
exports.deletePromotion = deletePromotion;
|
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
const crypto_1 = __importDefault(require("crypto"));
|
|
const otplib_1 = require("otplib");
|
|
const tokens_1 = require("../../security/tokens");
|
|
const qrcode_1 = __importDefault(require("qrcode"));
|
|
const platformContentService_1 = require("../../services/platformContentService");
|
|
const notificationService_1 = require("../../services/notificationService");
|
|
const presenter = __importStar(require("./admin.presenter"));
|
|
const repo = __importStar(require("./admin.repo"));
|
|
const billingService = __importStar(require("./admin.billing.service"));
|
|
const ADMIN_RESET_TTL_MINUTES = 60;
|
|
const ADMIN_RECOVERY_CODE_COUNT = 10;
|
|
function generateRecoveryCode() {
|
|
const raw = crypto_1.default.randomBytes(9).toString('base64url').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 12);
|
|
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
|
|
}
|
|
async function issueAdminRecoveryCodes(adminId) {
|
|
const codes = Array.from({ length: ADMIN_RECOVERY_CODE_COUNT }, generateRecoveryCode);
|
|
const hashes = await Promise.all(codes.map((code) => bcryptjs_1.default.hash(code, 12)));
|
|
await repo.replaceAdminRecoveryCodes(adminId, hashes);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'ADMIN_2FA_RECOVERY_CODES_ISSUED',
|
|
resource: 'AdminUser',
|
|
resourceId: adminId,
|
|
});
|
|
return codes;
|
|
}
|
|
async function consumeAdminRecoveryCode(adminId, code) {
|
|
const normalized = code.trim().toUpperCase();
|
|
if (!normalized)
|
|
return false;
|
|
const codes = await repo.listUnusedAdminRecoveryCodes(adminId);
|
|
for (const candidate of codes) {
|
|
if (await bcryptjs_1.default.compare(normalized, candidate.codeHash)) {
|
|
await repo.markAdminRecoveryCodeUsed(candidate.id);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'ADMIN_2FA_RECOVERY_CODE_USED',
|
|
resource: 'AdminUser',
|
|
resourceId: adminId,
|
|
});
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function signAdminToken(adminId, last2faAt) {
|
|
return (0, tokens_1.signActorToken)(adminId, 'admin', { expiresIn: '8h', last2faAt });
|
|
}
|
|
function toAuditJson(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
function ensureAdminBasePath(baseUrl) {
|
|
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`;
|
|
}
|
|
}
|
|
async function login(email, password, totpCode, recoveryCode) {
|
|
const admin = await repo.findAdminByEmail(email);
|
|
if (!admin || !admin.isActive)
|
|
return null;
|
|
const valid = await bcryptjs_1.default.compare(password, admin.passwordHash);
|
|
if (!valid)
|
|
return null;
|
|
if (admin.totpEnabled) {
|
|
if (!totpCode && !recoveryCode)
|
|
return { totpRequired: true };
|
|
const validTotp = totpCode
|
|
? otplib_1.authenticator.verify({ token: totpCode, secret: admin.totpSecret })
|
|
: false;
|
|
const validRecoveryCode = !validTotp && recoveryCode
|
|
? await consumeAdminRecoveryCode(admin.id, recoveryCode)
|
|
: false;
|
|
if (!validTotp && !validRecoveryCode) {
|
|
return { invalidTotp: true };
|
|
}
|
|
}
|
|
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, admin.totpEnabled ? Date.now() : undefined));
|
|
}
|
|
async function setupTotp(adminId, email) {
|
|
const secret = otplib_1.authenticator.generateSecret();
|
|
await repo.updateAdminTotpSecret(adminId, secret);
|
|
const otpauth = otplib_1.authenticator.keyuri(email, 'RentalDriveGo Admin', secret);
|
|
const qrCode = await qrcode_1.default.toDataURL(otpauth);
|
|
return { secret, qrCode };
|
|
}
|
|
async function verifyTotp(adminId, code) {
|
|
const admin = await repo.findAdminByIdOrThrow(adminId);
|
|
if (!admin.totpSecret)
|
|
return false;
|
|
const valid = otplib_1.authenticator.verify({ token: code, secret: admin.totpSecret });
|
|
if (!valid)
|
|
return false;
|
|
await repo.enableAdminTotp(adminId);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'ADMIN_2FA_VERIFIED',
|
|
resource: 'AdminUser',
|
|
resourceId: adminId,
|
|
});
|
|
const recoveryCodes = await issueAdminRecoveryCodes(adminId);
|
|
return {
|
|
...presenter.presentAdminSession({ ...admin, totpEnabled: true }, signAdminToken(adminId, Date.now())),
|
|
recoveryCodes,
|
|
};
|
|
}
|
|
async function regenerateRecoveryCodes(adminId) {
|
|
return { recoveryCodes: await issueAdminRecoveryCodes(adminId) };
|
|
}
|
|
async function forgotPassword(email) {
|
|
const admin = await repo.findAdminByEmail(email);
|
|
if (!admin || !admin.isActive)
|
|
return;
|
|
const rawToken = crypto_1.default.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 (0, notificationService_1.sendTransactionalEmail)({
|
|
to: admin.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));
|
|
}
|
|
async function resetPassword(token, password) {
|
|
const admin = await repo.findAdminByResetToken(token);
|
|
if (!admin)
|
|
return false;
|
|
await repo.updateAdminPassword(admin.id, await bcryptjs_1.default.hash(password, 12));
|
|
return true;
|
|
}
|
|
async function listCompanies(query) {
|
|
const { data, total } = await repo.listCompaniesPage(query);
|
|
return presenter.presentPaginated(data, total, query.page, query.pageSize);
|
|
}
|
|
function getCompany(id) {
|
|
return repo.getCompanyDetail(id);
|
|
}
|
|
async function updateCompany(id, body, adminId, ip) {
|
|
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;
|
|
}
|
|
async function setCompanyStatus(id, status, reason, adminId, ip) {
|
|
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;
|
|
}
|
|
async function deleteCompany(id, adminId, ip) {
|
|
await repo.deleteCompany(id);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'DELETE_COMPANY',
|
|
resource: 'Company',
|
|
resourceId: id,
|
|
ipAddress: ip,
|
|
});
|
|
}
|
|
async function impersonateCompany(id, adminId, ip, reason, durationMinutes = 15) {
|
|
const company = await repo.getCompanyForImpersonation(id);
|
|
const ttlMinutes = Math.min(Math.max(durationMinutes, 1), 30);
|
|
const employeeId = company.employees[0]?.id;
|
|
if (!employeeId)
|
|
throw new Error('Company has no employee account to impersonate');
|
|
const token = (0, tokens_1.signActorToken)(employeeId, 'employee', { expiresIn: `${ttlMinutes}m` });
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'IMPERSONATE_COMPANY',
|
|
resource: 'Company',
|
|
resourceId: id,
|
|
companyId: id,
|
|
note: reason,
|
|
before: { originalAdminId: adminId },
|
|
after: { targetCompanyId: id, durationMinutes: ttlMinutes },
|
|
ipAddress: ip,
|
|
});
|
|
return { token, expiresIn: ttlMinutes * 60, impersonation: { companyId: id, reason, durationMinutes: ttlMinutes } };
|
|
}
|
|
async function listRenters(query) {
|
|
const { data, total } = await repo.listRentersPage(query);
|
|
return presenter.presentPaginated(data, total, query.page, query.pageSize);
|
|
}
|
|
function setRenterActive(id, isActive) {
|
|
return repo.updateRenterActive(id, isActive);
|
|
}
|
|
function getPlatformMetrics() {
|
|
return repo.getPlatformMetricCounts();
|
|
}
|
|
async function listNotifications(query) {
|
|
const { data, total } = await repo.listNotificationsPage(query);
|
|
return presenter.presentPaginated(data, total, query.page, query.pageSize);
|
|
}
|
|
async function getAuditLogs(query) {
|
|
const { data, total } = await repo.listAuditLogsPage(query);
|
|
return presenter.presentPaginated(data, total, query.page, query.pageSize);
|
|
}
|
|
async function listAdmins() {
|
|
const admins = await repo.listAdmins();
|
|
return admins.map((admin) => presenter.presentAdminUser(admin));
|
|
}
|
|
async function createAdmin(body) {
|
|
const admin = await repo.createAdmin({
|
|
...body,
|
|
passwordHash: await bcryptjs_1.default.hash(body.password, 12),
|
|
});
|
|
return presenter.presentAdminUser(admin);
|
|
}
|
|
async function updateAdmin(id, body) {
|
|
const admin = await repo.updateAdmin(id, {
|
|
...(body.email !== undefined ? { email: body.email } : {}),
|
|
...(body.firstName !== undefined ? { firstName: body.firstName } : {}),
|
|
...(body.lastName !== undefined ? { lastName: body.lastName } : {}),
|
|
...(body.role !== undefined ? { role: body.role } : {}),
|
|
...(body.isActive !== undefined ? { isActive: body.isActive } : {}),
|
|
...(body.password ? { passwordHash: await bcryptjs_1.default.hash(body.password, 12) } : {}),
|
|
});
|
|
return presenter.presentAdminUser(admin);
|
|
}
|
|
function updateAdminRole(id, role) {
|
|
return repo.updateAdminRole(id, role);
|
|
}
|
|
async function updateAdminPermissions(id, permissions) {
|
|
const admin = await repo.replaceAdminPermissions(id, permissions);
|
|
return presenter.presentAdminUser(admin);
|
|
}
|
|
async function getBilling(query) {
|
|
const { data, total, stats } = await billingService.listBillingAccounts(query);
|
|
return presenter.presentPaginated(data, total, query.page, query.pageSize, { stats });
|
|
}
|
|
async function getCompanyInvoices(companyId, query) {
|
|
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);
|
|
}
|
|
async function getInvoicePdf(invoiceId) {
|
|
return billingService.getInvoicePdf(invoiceId);
|
|
}
|
|
function getBillingAccountDetail(companyId) {
|
|
return billingService.getBillingAccountDetail(companyId);
|
|
}
|
|
function updateBillingAccount(billingAccountId, data, adminId, ip) {
|
|
return billingService.updateBillingAccount(billingAccountId, data, adminId, ip);
|
|
}
|
|
function setDunningPaused(billingAccountId, paused, adminId, ip) {
|
|
return billingService.setDunningPaused(billingAccountId, paused, adminId, ip);
|
|
}
|
|
function createBillingInvoice(billingAccountId, data, adminId, ip) {
|
|
return billingService.createDraftInvoice(billingAccountId, data, adminId, ip);
|
|
}
|
|
function finalizeBillingInvoice(invoiceId, adminId, ip) {
|
|
return billingService.finalizeInvoice(invoiceId, adminId, ip);
|
|
}
|
|
function payBillingInvoice(invoiceId, data, adminId, ip) {
|
|
return billingService.payInvoice(invoiceId, data, adminId, ip);
|
|
}
|
|
function retryBillingInvoicePayment(invoiceId, data, adminId, ip) {
|
|
return billingService.retryInvoicePayment(invoiceId, data, adminId, ip);
|
|
}
|
|
function voidBillingInvoice(invoiceId, reason, adminId, ip) {
|
|
return billingService.voidInvoice(invoiceId, reason, adminId, ip);
|
|
}
|
|
function markBillingInvoiceUncollectible(invoiceId, reason, adminId, ip) {
|
|
return billingService.markInvoiceUncollectible(invoiceId, reason, adminId, ip);
|
|
}
|
|
function issueBillingCreditNote(invoiceId, data, adminId, ip) {
|
|
return billingService.issueCreditNote(invoiceId, data, adminId, ip);
|
|
}
|
|
function issueBillingRefund(invoiceId, data, adminId, ip) {
|
|
return billingService.issueRefund(invoiceId, data, adminId, ip);
|
|
}
|
|
function getMarketplaceHomepage() {
|
|
return (0, platformContentService_1.getMarketplaceHomepageContent)();
|
|
}
|
|
async function updateMarketplaceHomepage(homepage, adminId, ip) {
|
|
const saved = await (0, platformContentService_1.saveMarketplaceHomepageContent)(homepage);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'UPDATE',
|
|
resource: 'MarketplaceHomepage',
|
|
after: toAuditJson(saved),
|
|
ipAddress: ip,
|
|
userAgent: undefined,
|
|
});
|
|
return saved;
|
|
}
|
|
function getPricingConfigs() {
|
|
return repo.listPricingConfigs();
|
|
}
|
|
async function updatePricingConfigs(entries, adminId, ip) {
|
|
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;
|
|
}
|
|
function listPlanFeatures() {
|
|
return repo.listPlanFeatures();
|
|
}
|
|
async function createPlanFeature(data, adminId, ip) {
|
|
const feature = await repo.createPlanFeature(data);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'CREATE',
|
|
resource: 'PlanFeature',
|
|
after: toAuditJson(feature),
|
|
ipAddress: ip,
|
|
userAgent: undefined,
|
|
});
|
|
return feature;
|
|
}
|
|
async function updatePlanFeature(id, data, adminId, ip) {
|
|
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;
|
|
}
|
|
async function deletePlanFeature(id, adminId, ip) {
|
|
await repo.deletePlanFeature(id);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId,
|
|
action: 'DELETE',
|
|
resource: 'PlanFeature',
|
|
resourceId: id,
|
|
ipAddress: ip,
|
|
userAgent: undefined,
|
|
});
|
|
}
|
|
// ─── Promotions ────────────────────────────────────────────────────────────
|
|
function listPromotions() {
|
|
return repo.listPromotions();
|
|
}
|
|
async function createPromotion(data, adminId, ip) {
|
|
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;
|
|
}
|
|
async function updatePromotion(id, data, adminId, ip) {
|
|
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;
|
|
}
|
|
async function deletePromotion(id, adminId, ip) {
|
|
await repo.deletePromotion(id);
|
|
await repo.createAuditLog({
|
|
adminUserId: adminId, action: 'DELETE', resource: 'PricingPromotion',
|
|
entityId: id, ipAddress: ip, userAgent: undefined,
|
|
});
|
|
}
|
|
//# sourceMappingURL=admin.service.js.map
|