Files
alrahma_sunday_school_api/apps/api/dist/modules/admin/admin.service.js
T
2026-06-11 03:22:12 -04:00

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