archetecture security fix

This commit is contained in:
root
2026-06-11 03:22:12 -04:00
parent 6def9993da
commit 9483750161
3126 changed files with 177194 additions and 37211 deletions
+3
View File
@@ -0,0 +1,3 @@
export declare const corsOrigins: string[];
export declare function createApp(): import("express-serve-static-core").Express;
//# sourceMappingURL=app.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,WAAW,UAEF,CAAA;AAyBtB,wBAAgB,SAAS,gDAmIxB"}
+218
View File
@@ -0,0 +1,218 @@
"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.corsOrigins = void 0;
exports.createApp = createApp;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const helmet_1 = __importDefault(require("helmet"));
const morgan_1 = __importDefault(require("morgan"));
const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
const openapi_1 = require("./swagger/openapi");
const storage_1 = require("./lib/storage");
const rateLimiter_1 = require("./middleware/rateLimiter");
const requestId_1 = require("./middleware/requestId");
// ─── Module routes ────────────────────────────────────────────
const webhook_routes_1 = __importDefault(require("./modules/webhooks/webhook.routes"));
const auth_company_routes_1 = __importDefault(require("./modules/auth/auth.company.routes"));
const auth_employee_routes_1 = __importDefault(require("./modules/auth/auth.employee.routes"));
const auth_renter_routes_1 = __importDefault(require("./modules/auth/auth.renter.routes"));
const team_routes_1 = __importDefault(require("./modules/team/team.routes"));
const offer_routes_1 = __importDefault(require("./modules/offers/offer.routes"));
const analytics_routes_1 = __importDefault(require("./modules/analytics/analytics.routes"));
const notification_routes_1 = __importDefault(require("./modules/notifications/notification.routes"));
const admin_routes_1 = __importDefault(require("./modules/admin/admin.routes"));
const subscription_routes_1 = __importStar(require("./modules/subscriptions/subscription.routes"));
const payment_routes_1 = __importDefault(require("./modules/payments/payment.routes"));
const customer_routes_1 = __importDefault(require("./modules/customers/customer.routes"));
const vehicle_routes_1 = __importDefault(require("./modules/vehicles/vehicle.routes"));
const company_routes_1 = __importDefault(require("./modules/companies/company.routes"));
const reservation_routes_1 = __importDefault(require("./modules/reservations/reservation.routes"));
const marketplace_routes_1 = __importDefault(require("./modules/marketplace/marketplace.routes"));
const site_routes_1 = __importDefault(require("./modules/site/site.routes"));
const review_routes_1 = __importDefault(require("./modules/reviews/review.routes"));
const complaint_routes_1 = __importDefault(require("./modules/complaints/complaint.routes"));
// ─── Centralized error handling ───────────────────────────────
const errorMiddleware_1 = require("./http/errors/errorMiddleware");
const v1 = '/api/v1';
const defaultCorsOrigins = [
'http://localhost:3000',
'http://localhost:4000',
'http://127.0.0.1:3000',
'http://127.0.0.1:4000',
];
exports.corsOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',').map((o) => o.trim()).filter(Boolean)
: defaultCorsOrigins;
const routeDocs = [
{ method: 'GET', path: '/health', description: 'Health check' },
{ method: 'GET', path: `${v1}/docs`, description: 'Machine-readable API index' },
{ method: 'GET', path: `${v1}/auth/renter/me`, description: 'Current renter profile' },
{ method: 'GET', path: `${v1}/vehicles`, description: 'List company vehicles' },
{ method: 'POST', path: `${v1}/vehicles`, description: 'Create vehicle' },
{ method: 'GET', path: `${v1}/reservations`, description: 'List reservations' },
{ method: 'POST', path: `${v1}/reservations`, description: 'Create reservation' },
{ method: 'GET', path: `${v1}/customers`, description: 'List customers' },
{ method: 'POST', path: `${v1}/customers`, description: 'Create customer' },
{ method: 'GET', path: `${v1}/offers`, description: 'List offers' },
{ method: 'GET', path: `${v1}/analytics/dashboard`, description: 'Dashboard analytics' },
{ method: 'GET', path: `${v1}/analytics/report`, description: 'Analytics report' },
{ method: 'GET', path: `${v1}/notifications/company`, description: 'Company notifications' },
{ method: 'GET', path: `${v1}/marketplace/cities`, description: 'Marketplace cities' },
{ method: 'GET', path: `${v1}/marketplace/search`, description: 'Marketplace search' },
{ method: 'GET', path: `${v1}/site/:slug/brand`, description: 'Public site brand config' },
{ method: 'GET', path: `${v1}/companies/me`, description: 'Current company profile' },
{ method: 'GET', path: `${v1}/subscriptions/plans`, description: 'Subscription plans' },
{ method: 'GET', path: `${v1}/subscriptions/features`, description: 'Subscription plan features' },
{ method: 'POST', path: `${v1}/admin/auth/login`, description: 'Admin login' },
];
function createApp() {
const app = (0, express_1.default)();
const corsMiddleware = (0, cors_1.default)({ origin: exports.corsOrigins, credentials: true });
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
app.use(requestId_1.requestIdMiddleware);
app.use((req, res, next) => {
if (req.headers['x-middleware-subrequest']) {
return res.status(400).json({ error: 'bad_request', message: 'Unsupported internal request header', statusCode: 400 });
}
next();
});
app.use(corsMiddleware);
// Customer identity documents must never be anonymously retrievable from the
// public storage mount, even if an older raw storage URL leaks.
app.use('/storage/companies/:companyId/customers/:customerId', (_req, res) => {
res.status(404).end();
});
// Public storage assets (logos, vehicle photos, etc.) must be loadable cross-origin
// by the browser. Without this header, helmet's default CORP: same-origin reaches
// 404 responses (when express.static calls next() on a miss) and the browser blocks
// the response even for broken-image cases, producing ERR_BLOCKED_BY_RESPONSE.
app.use('/storage', (_req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
next();
});
// Serve only explicitly public uploaded assets. Private documents are resolved
// through authenticated API routes such as /customers/:id/license-image.
app.use('/storage', express_1.default.static((0, storage_1.getPublicStorageRoot)()));
// Swagger UI — mounted before helmet so its assets are not blocked by CSP
app.use('/docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(openapi_1.openApiDocument, { customSiteTitle: 'RentalDriveGo API Docs' }));
app.get('/api/v1/openapi.json', (_req, res) => res.json(openapi_1.openApiDocument));
// Webhooks must use raw body BEFORE express.json(); signature verification
// must never reconstruct the payload with JSON.stringify(req.body).
app.use(`${v1}/webhooks`, express_1.default.raw({ type: 'application/json' }), webhook_routes_1.default);
app.use(`${v1}/payments/webhooks`, express_1.default.raw({ type: 'application/json' }));
app.use(`${v1}/subscriptions/webhooks`, express_1.default.raw({ type: 'application/json' }));
// Let /storage responses manage CORP explicitly so missing files still return
// a normal cross-origin 404 instead of being blocked by Helmet's default
// same-origin policy. Keep CSP explicit instead of letting browser security
// drift into wishful thinking with headers.
app.use((0, helmet_1.default)({
crossOriginResourcePolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
objectSrc: ["'none'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
connectSrc: ["'self'", 'https:', 'wss:'],
upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null,
},
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
frameguard: { action: 'deny' },
}));
if (process.env.NODE_ENV !== 'test')
app.use((0, morgan_1.default)('combined'));
app.use(express_1.default.json({ limit: '10mb' }));
// ─── API Routes ─────────────────────────────────────────────
app.use(`${v1}/auth/renter`, rateLimiter_1.authLimiter, auth_renter_routes_1.default);
app.use(`${v1}/auth/company`, rateLimiter_1.authLimiter, auth_company_routes_1.default);
app.use(`${v1}/auth/employee`, rateLimiter_1.authLimiter, auth_employee_routes_1.default);
app.use(`${v1}/admin/auth`, rateLimiter_1.authLimiter);
app.use(`${v1}/admin`, rateLimiter_1.adminLimiter, admin_routes_1.default);
app.use(`${v1}/marketplace`, rateLimiter_1.publicLimiter, marketplace_routes_1.default);
app.use(`${v1}/site`, rateLimiter_1.publicLimiter, site_routes_1.default);
app.use(`${v1}/subscriptions`, subscription_routes_1.subscriptionPublicRouter);
app.use(`${v1}/subscriptions`, subscription_routes_1.subscriptionWebhookRouter);
app.use(`${v1}/vehicles`, rateLimiter_1.apiLimiter, vehicle_routes_1.default);
app.use(`${v1}/reservations`, rateLimiter_1.apiLimiter, reservation_routes_1.default);
app.use(`${v1}/team`, rateLimiter_1.apiLimiter, team_routes_1.default);
app.use(`${v1}/customers`, rateLimiter_1.apiLimiter, customer_routes_1.default);
app.use(`${v1}/offers`, rateLimiter_1.apiLimiter, offer_routes_1.default);
app.use(`${v1}/analytics`, rateLimiter_1.apiLimiter, analytics_routes_1.default);
app.use(`${v1}/notifications`, rateLimiter_1.apiLimiter, notification_routes_1.default);
app.use(`${v1}/companies`, rateLimiter_1.apiLimiter, company_routes_1.default);
app.use(`${v1}/subscriptions`, rateLimiter_1.apiLimiter, subscription_routes_1.default);
app.use(`${v1}/payments`, rateLimiter_1.apiLimiter, payment_routes_1.default);
app.use(`${v1}/reviews`, rateLimiter_1.apiLimiter, review_routes_1.default);
app.use(`${v1}/complaints`, rateLimiter_1.apiLimiter, complaint_routes_1.default);
// ─── Health / Docs ──────────────────────────────────────────
app.get(v1, (_req, res) => {
res.json({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: v1,
health: '/health',
docs: `${v1}/docs`,
openApi: `${v1}/openapi.json`,
});
});
app.get('/health', (_req, res) => {
res.json({ status: 'ok', version: '1.0.0', timestamp: new Date().toISOString() });
});
app.get(`${v1}/docs`, (_req, res) => {
res.json({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: v1,
routes: routeDocs,
swaggerUi: '/docs',
openApi: '/api/v1/openapi.json',
});
});
// ─── Error handler ──────────────────────────────────────────
app.use(errorMiddleware_1.errorMiddleware);
return app;
}
//# sourceMappingURL=app.js.map
+1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
import { Request, Response, NextFunction } from 'express';
export declare function errorMiddleware(err: any, req: Request, res: Response, _next: NextFunction): Response<any, Record<string, any>> | undefined;
//# sourceMappingURL=errorMiddleware.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"errorMiddleware.d.ts","sourceRoot":"","sources":["../../../src/http/errors/errorMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAOzD,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,kDAwCzF"}
+44
View File
@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.errorMiddleware = errorMiddleware;
const index_1 = require("./index");
function withRequestId(req, payload) {
return { ...payload, requestId: req.requestId };
}
function errorMiddleware(err, req, res, _next) {
if (err.name === 'ZodError') {
return res.status(400).json(withRequestId(req, {
error: 'validation_error',
message: 'Invalid request body',
issues: err.issues,
statusCode: 400,
}));
}
if (err.code === 'P2025') {
return res.status(404).json(withRequestId(req, { error: 'not_found', message: 'Resource not found', statusCode: 404 }));
}
if (err.code === 'P2002') {
return res.status(409).json(withRequestId(req, { error: 'conflict', message: 'A resource with this value already exists', statusCode: 409 }));
}
if (err instanceof index_1.AppError) {
if (err.statusCode >= 500)
console.error('[API Error]', { requestId: req.requestId, err });
return res.status(err.statusCode).json(withRequestId(req, {
error: err.error,
message: err.statusCode >= 500 ? 'Internal server error' : err.message,
statusCode: err.statusCode,
...(err.statusCode >= 500 ? {} : err.data),
}));
}
const statusCode = typeof err.statusCode === 'number' ? err.statusCode : 500;
const safeStatusCode = statusCode >= 400 && statusCode < 600 ? statusCode : 500;
if (safeStatusCode >= 500) {
console.error('[API Error]', { requestId: req.requestId, err });
}
res.status(safeStatusCode).json(withRequestId(req, {
error: safeStatusCode >= 500 ? 'internal_error' : (err.code ?? 'request_error'),
message: safeStatusCode >= 500 ? 'Internal server error' : (err.message ?? 'Request failed'),
statusCode: safeStatusCode,
}));
}
//# sourceMappingURL=errorMiddleware.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"errorMiddleware.js","sourceRoot":"","sources":["../../../src/http/errors/errorMiddleware.ts"],"names":[],"mappings":"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"}
+22
View File
@@ -0,0 +1,22 @@
export declare class AppError extends Error {
readonly statusCode: number;
readonly error: string;
readonly data?: Record<string, unknown>;
constructor(message: string, statusCode: number, error: string, data?: Record<string, unknown>);
}
export declare class ValidationError extends AppError {
constructor(message?: string);
}
export declare class NotFoundError extends AppError {
constructor(message?: string);
}
export declare class ConflictError extends AppError {
constructor(message?: string);
}
export declare class ForbiddenError extends AppError {
constructor(message?: string);
}
export declare class UnauthorizedError extends AppError {
constructor(message?: string);
}
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/errors/index.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;gBAE3B,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAO/F;AAED,qBAAa,eAAgB,SAAQ,QAAQ;gBAC/B,OAAO,SAAqB;CAGzC;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,OAAO,SAAuB;CAG3C;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,OAAO,SAA8C;CAGlE;AAED,qBAAa,cAAe,SAAQ,QAAQ;gBAC9B,OAAO,SAAc;CAGlC;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;gBACjC,OAAO,SAAiB;CAGrC"}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnauthorizedError = exports.ForbiddenError = exports.ConflictError = exports.NotFoundError = exports.ValidationError = exports.AppError = void 0;
class AppError extends Error {
statusCode;
error;
data;
constructor(message, statusCode, error, data) {
super(message);
this.statusCode = statusCode;
this.error = error;
this.data = data;
this.name = this.constructor.name;
}
}
exports.AppError = AppError;
class ValidationError extends AppError {
constructor(message = 'Validation error') {
super(message, 400, 'validation_error');
}
}
exports.ValidationError = ValidationError;
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404, 'not_found');
}
}
exports.NotFoundError = NotFoundError;
class ConflictError extends AppError {
constructor(message = 'A resource with this value already exists') {
super(message, 409, 'conflict');
}
}
exports.ConflictError = ConflictError;
class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403, 'forbidden');
}
}
exports.ForbiddenError = ForbiddenError;
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'unauthorized');
}
}
exports.UnauthorizedError = UnauthorizedError;
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/errors/index.ts"],"names":[],"mappings":";;;AAAA,MAAa,QAAS,SAAQ,KAAK;IACxB,UAAU,CAAQ;IAClB,KAAK,CAAQ;IACb,IAAI,CAA0B;IAEvC,YAAY,OAAe,EAAE,UAAkB,EAAE,KAAa,EAAE,IAA8B;QAC5F,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAA;IACnC,CAAC;CACF;AAZD,4BAYC;AAED,MAAa,eAAgB,SAAQ,QAAQ;IAC3C,YAAY,OAAO,GAAG,kBAAkB;QACtC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,kBAAkB,CAAC,CAAA;IACzC,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,aAAc,SAAQ,QAAQ;IACzC,YAAY,OAAO,GAAG,oBAAoB;QACxC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IAClC,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,QAAQ;IACzC,YAAY,OAAO,GAAG,2CAA2C;QAC/D,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,CAAA;IACjC,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,cAAe,SAAQ,QAAQ;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IAClC,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,iBAAkB,SAAQ,QAAQ;IAC7C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,CAAC,CAAA;IACrC,CAAC;CACF;AAJD,8CAIC"}
+5
View File
@@ -0,0 +1,5 @@
import { Response } from 'express';
export declare function ok<T>(res: Response, data: T): void;
export declare function created<T>(res: Response, data: T): void;
export declare function noContent(res: Response): void;
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/respond/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,wBAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAElD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAEvD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAE7C"}
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ok = ok;
exports.created = created;
exports.noContent = noContent;
function ok(res, data) {
res.json({ data });
}
function created(res, data) {
res.status(201).json({ data });
}
function noContent(res) {
res.status(204).end();
}
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/respond/index.ts"],"names":[],"mappings":";;AAEA,gBAEC;AAED,0BAEC;AAED,8BAEC;AAVD,SAAgB,EAAE,CAAI,GAAa,EAAE,IAAO;IAC1C,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;AACpB,CAAC;AAED,SAAgB,OAAO,CAAI,GAAa,EAAE,IAAO;IAC/C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;AAChC,CAAC;AAED,SAAgB,SAAS,CAAC,GAAa;IACrC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA;AACvB,CAAC"}
+21
View File
@@ -0,0 +1,21 @@
import multer from 'multer';
/**
* Shared multer instance used by all upload endpoints.
* Files are kept in memory so services receive a Buffer — no temp-file cleanup needed.
*/
export declare const imageUpload: multer.Multer;
type DetectedFile = {
mime: string;
ext: string;
};
export declare function detectImageType(buffer: Buffer): DetectedFile | null;
/**
* Asserts that a file was provided and is an image by content, not by client claims.
*/
export declare function assertImageFile(file: Express.Multer.File | undefined, fieldLabel?: string): asserts file is Express.Multer.File;
/**
* Asserts that at least one file was provided and all files are image content.
*/
export declare function assertImageFiles(files: Express.Multer.File[], fieldLabel?: string): void;
export {};
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/upload/index.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAW3B;;;GAGG;AACH,eAAO,MAAM,WAAW,eAGtB,CAAA;AAEF,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuBnE;AAmBD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EACrC,UAAU,SAAS,GAClB,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAGrC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,UAAU,SAAW,GAAG,IAAI,CAG1F"}
+78
View File
@@ -0,0 +1,78 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.imageUpload = void 0;
exports.detectImageType = detectImageType;
exports.assertImageFile = assertImageFile;
exports.assertImageFiles = assertImageFiles;
const path_1 = __importDefault(require("path"));
const multer_1 = __importDefault(require("multer"));
const errors_1 = require("../errors");
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
const ALLOWED_IMAGE_TYPES = new Map([
['image/jpeg', ['.jpg', '.jpeg']],
['image/png', ['.png']],
['image/webp', ['.webp']],
['image/gif', ['.gif']],
]);
/**
* Shared multer instance used by all upload endpoints.
* Files are kept in memory so services receive a Buffer — no temp-file cleanup needed.
*/
exports.imageUpload = (0, multer_1.default)({
storage: multer_1.default.memoryStorage(),
limits: { fileSize: MAX_FILE_SIZE, files: 20 },
});
function detectImageType(buffer) {
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return { mime: 'image/jpeg', ext: '.jpg' };
}
if (buffer.length >= 8 &&
buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 &&
buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a) {
return { mime: 'image/png', ext: '.png' };
}
if (buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') {
return { mime: 'image/webp', ext: '.webp' };
}
if (buffer.length >= 6) {
const sig = buffer.subarray(0, 6).toString('ascii');
if (sig === 'GIF87a' || sig === 'GIF89a')
return { mime: 'image/gif', ext: '.gif' };
}
return null;
}
function assertSafeImageContent(file) {
const detected = detectImageType(file.buffer);
if (!detected || !ALLOWED_IMAGE_TYPES.has(detected.mime)) {
throw new errors_1.ValidationError('Unsupported or spoofed image file');
}
if (file.mimetype !== detected.mime) {
throw new errors_1.ValidationError(`MIME type does not match file content for "${file.originalname}"`);
}
const ext = path_1.default.extname(file.originalname).toLowerCase();
const allowedExtensions = ALLOWED_IMAGE_TYPES.get(detected.mime) ?? [];
if (ext && !allowedExtensions.includes(ext)) {
throw new errors_1.ValidationError(`File extension does not match file content for "${file.originalname}"`);
}
}
/**
* Asserts that a file was provided and is an image by content, not by client claims.
*/
function assertImageFile(file, fieldLabel = 'file') {
if (!file)
throw new errors_1.ValidationError(`A ${fieldLabel} is required`);
assertSafeImageContent(file);
}
/**
* Asserts that at least one file was provided and all files are image content.
*/
function assertImageFiles(files, fieldLabel = 'photos') {
if (!files || files.length === 0)
throw new errors_1.ValidationError(`At least one ${fieldLabel} file is required`);
for (const file of files)
assertSafeImageContent(file);
}
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/upload/index.ts"],"names":[],"mappings":"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"}
+6
View File
@@ -0,0 +1,6 @@
import type { Request } from 'express';
import type { ZodTypeAny, output } from 'zod';
export declare function parseBody<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema>;
export declare function parseQuery<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema>;
export declare function parseParams<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema>;
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/validate/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAE7C,wBAAgB,SAAS,CAAC,OAAO,SAAS,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAEpG;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAErG;AAED,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAEtG"}
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseBody = parseBody;
exports.parseQuery = parseQuery;
exports.parseParams = parseParams;
function parseBody(schema, req) {
return schema.parse(req.body);
}
function parseQuery(schema, req) {
return schema.parse(req.query);
}
function parseParams(schema, req) {
return schema.parse(req.params);
}
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/validate/index.ts"],"names":[],"mappings":";;AAGA,8BAEC;AAED,gCAEC;AAED,kCAEC;AAVD,SAAgB,SAAS,CAA6B,MAAe,EAAE,GAAY;IACjF,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC/B,CAAC;AAED,SAAgB,UAAU,CAA6B,MAAe,EAAE,GAAY;IAClF,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AAChC,CAAC;AAED,SAAgB,WAAW,CAA6B,MAAe,EAAE,GAAY;IACnF,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC"}
+4
View File
@@ -0,0 +1,4 @@
import type { Request } from 'express';
export declare function getRawBodyString(req: Request): string;
export declare function parseRawJsonBody<T = unknown>(req: Request): T;
//# sourceMappingURL=webhooks.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/http/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAGtC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,UAK5C;AAED,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,CAAC,CAO7D"}
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRawBodyString = getRawBodyString;
exports.parseRawJsonBody = parseRawJsonBody;
const errors_1 = require("./errors");
function getRawBodyString(req) {
if (!Buffer.isBuffer(req.body)) {
throw new errors_1.ValidationError('Webhook route must be mounted with express.raw before JSON parsing');
}
return req.body.toString('utf8');
}
function parseRawJsonBody(req) {
const rawBody = getRawBodyString(req);
try {
return JSON.parse(rawBody);
}
catch {
throw new errors_1.ValidationError('Malformed webhook JSON');
}
}
//# sourceMappingURL=webhooks.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/http/webhooks.ts"],"names":[],"mappings":";;AAGA,4CAKC;AAED,4CAOC;AAhBD,qCAA0C;AAE1C,SAAgB,gBAAgB,CAAC,GAAY;IAC3C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,wBAAe,CAAC,oEAAoE,CAAC,CAAA;IACjG,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;AAClC,CAAC;AAED,SAAgB,gBAAgB,CAAc,GAAY;IACxD,MAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;IACrC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAM,CAAA;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,wBAAe,CAAC,wBAAwB,CAAC,CAAA;IACrD,CAAC;AACH,CAAC"}
+5
View File
@@ -0,0 +1,5 @@
import { Server as SocketIOServer } from 'socket.io';
declare const app: import("express-serve-static-core").Express;
declare const io: SocketIOServer<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
export { app, io };
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,WAAW,CAAA;AAkBpD,QAAA,MAAM,GAAG,6CAAiB,CAAA;AAK1B,QAAA,MAAM,EAAE,uIAEN,CAAA;AAoPF,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAA"}
+259
View File
@@ -0,0 +1,259 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.io = exports.app = void 0;
const http_1 = __importDefault(require("http"));
const socket_io_1 = require("socket.io");
const node_cron_1 = __importDefault(require("node-cron"));
const redis_1 = require("./lib/redis");
const prisma_1 = require("./lib/prisma");
const storage_1 = require("./lib/storage");
const app_1 = require("./app");
const tokens_1 = require("./security/tokens");
const sessionCookies_1 = require("./security/sessionCookies");
const notificationService_1 = require("./services/notificationService");
const subscription_service_1 = require("./modules/subscriptions/subscription.service");
const app = (0, app_1.createApp)();
exports.app = app;
const server = http_1.default.createServer(app);
(0, storage_1.assertStorageConfiguration)();
// ─── Socket.io ────────────────────────────────────────────────
const io = new socket_io_1.Server(server, {
cors: { origin: app_1.corsOrigins, credentials: true, methods: ['GET', 'POST'] },
});
exports.io = io;
function readCookieFromHeader(cookieHeader, name) {
if (!cookieHeader)
return null;
for (const chunk of cookieHeader.split(';')) {
const [rawName, ...rawValue] = chunk.trim().split('=');
if (rawName === name)
return decodeURIComponent(rawValue.join('='));
}
return null;
}
function getSocketSessionToken(socket) {
const explicitToken = socket.handshake.auth?.token;
if (typeof explicitToken === 'string' && explicitToken.trim())
return explicitToken.trim();
const cookieHeader = socket.request.headers.cookie;
return (readCookieFromHeader(cookieHeader, (0, sessionCookies_1.getSessionCookieName)('employee')) ??
readCookieFromHeader(cookieHeader, (0, sessionCookies_1.getSessionCookieName)('admin')) ??
readCookieFromHeader(cookieHeader, (0, sessionCookies_1.getSessionCookieName)('renter')) ??
undefined);
}
// Authenticate socket connections via JWT before joining user rooms
io.use((socket, next) => {
const token = getSocketSessionToken(socket);
if (!token)
return next(); // unauthenticated connections allowed; they just don't join rooms
try {
const payload = (0, tokens_1.verifyAnyActorToken)(token);
socket.authenticatedUserId = payload.sub;
next();
}
catch {
next(new Error('invalid_token'));
}
});
io.on('connection', (socket) => {
const userId = socket.authenticatedUserId;
if (userId) {
socket.join(`user:${userId}`);
}
});
// Redis pub/sub → broadcast to connected clients
const subscriber = redis_1.redis.duplicate();
subscriber.psubscribe('notifications:*', (err) => {
if (err)
console.error('[Redis] Subscribe error:', err);
});
subscriber.on('pmessage', (_pattern, channel, message) => {
try {
const parsed = JSON.parse(message);
const userId = channel.replace('notifications:', '');
io.to(`user:${userId}`).emit('notification', parsed);
}
catch (err) {
console.error('[Redis] Invalid notification message:', err);
}
});
// ─── Scheduled jobs ───────────────────────────────────────────
// Daily: flag expiring/expired licenses
node_cron_1.default.schedule('0 8 * * *', async () => {
const customers = await prisma_1.prisma.customer.findMany({ where: { licenseExpiry: { not: null } } });
for (const c of customers) {
if (!c.licenseExpiry)
continue;
const daysLeft = Math.ceil((c.licenseExpiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
const expired = c.licenseExpiry <= new Date();
const expiring = !expired && daysLeft < 90;
if (expired !== c.licenseExpired || expiring !== c.licenseExpiringSoon) {
await prisma_1.prisma.customer.update({ where: { id: c.id }, data: { licenseExpired: expired, licenseExpiringSoon: expiring, licenseValidationStatus: expired ? 'EXPIRED' : expiring ? 'EXPIRING' : 'VALID' } });
}
}
});
// Hourly: expire trials that ended without payment
node_cron_1.default.schedule('0 * * * *', async () => {
const n = await (0, subscription_service_1.runTrialExpirationJob)();
if (n > 0)
console.log(`[subscription] trial_expiration: ${n} expired`);
});
// Hourly: payment_pending → past_due after 7 days
node_cron_1.default.schedule('15 * * * *', async () => {
const n = await (0, subscription_service_1.runPaymentPendingTimeoutJob)();
if (n > 0)
console.log(`[subscription] payment_pending_timeout: ${n} moved to past_due`);
});
// Hourly: past_due → suspended after 7 days
node_cron_1.default.schedule('30 * * * *', async () => {
const n = await (0, subscription_service_1.runPastDueTimeoutJob)();
if (n > 0)
console.log(`[subscription] past_due_timeout: ${n} suspended`);
});
// Daily: suspended → cancelled after 16 days; and period-end cancellations
node_cron_1.default.schedule('0 1 * * *', async () => {
const nSuspend = await (0, subscription_service_1.runSuspensionTimeoutJob)();
const nPeriod = await (0, subscription_service_1.runPeriodEndCancellationJob)();
if (nSuspend > 0)
console.log(`[subscription] suspension_timeout: ${nSuspend} cancelled`);
if (nPeriod > 0)
console.log(`[subscription] period_end_cancel: ${nPeriod} cancelled`);
});
// Daily: send trial-ending reminders (3 days before trial end)
node_cron_1.default.schedule('0 9 * * *', async () => {
const soon = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
const subscriptions = await prisma_1.prisma.subscription.findMany({
where: { status: 'TRIALING', trialEndAt: { lte: soon, gte: new Date() } },
include: { company: { include: { employees: { where: { role: 'OWNER' } } } } },
});
for (const sub of subscriptions) {
const owner = sub.company.employees[0];
if (owner) {
await (0, notificationService_1.sendNotification)({
type: 'SUBSCRIPTION_TRIAL_ENDING',
companyId: sub.companyId,
employeeId: owner.id,
channels: ['IN_APP'],
templateKey: 'subscription.trial_ending',
templateVariables: {
trialEndDate: sub.trialEndAt ?? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
},
}).catch((err) => {
console.error('[Notifications] Failed to create trial ending reminder:', err?.message ?? String(err));
});
}
}
});
// Daily: notify companies about upcoming and overdue vehicle maintenance (date- and odometer-based).
// Repeats every day until the owner logs a new service entry that pushes the due date/mileage into the future.
node_cron_1.default.schedule('0 8 * * *', async () => {
const now = new Date();
const in30Days = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// Fetch all candidate logs (date-due or has an odometer target), ordered newest-first per vehicle+type.
// We keep only the LATEST log per vehicle+type so that once the owner logs a new service the
// old overdue log is superseded and notifications stop automatically.
const allCandidates = await prisma_1.prisma.maintenanceLog.findMany({
where: {
OR: [
{ nextDueAt: { lte: in30Days } },
{ nextDueMileage: { not: null } },
],
},
include: { vehicle: { include: { company: { include: { employees: { where: { role: { in: ['OWNER', 'MANAGER'] }, isActive: true }, take: 1 } } } } } },
orderBy: { performedAt: 'desc' },
});
// Keep only the most-recent log per vehicle+type combination
const latestByKey = new Map();
for (const log of allCandidates) {
const key = `${log.vehicleId}:${log.type}`;
if (!latestByKey.has(key))
latestByKey.set(key, log);
}
for (const log of latestByKey.values()) {
const vehicle = log.vehicle;
const company = vehicle.company;
const recipient = company.employees[0];
if (!recipient)
continue;
// Determine date-based urgency
let isOverdueByDate = false;
let daysLeft = null;
let dueSoonByDate = false;
if (log.nextDueAt) {
daysLeft = Math.ceil((log.nextDueAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
isOverdueByDate = log.nextDueAt <= now;
dueSoonByDate = !isOverdueByDate && daysLeft <= 30;
}
// Determine odometer-based urgency
let isOverdueByOdometer = false;
let kmLeft = null;
let dueSoonByOdometer = false;
if (log.nextDueMileage != null && vehicle.mileage != null) {
kmLeft = log.nextDueMileage - vehicle.mileage;
isOverdueByOdometer = kmLeft <= 0;
dueSoonByOdometer = !isOverdueByOdometer && kmLeft <= 500;
}
// Skip if the latest log is no longer due (owner has updated it)
const isOverdue = isOverdueByDate || isOverdueByOdometer;
const isDueSoon = !isOverdue && (dueSoonByDate || dueSoonByOdometer);
if (!isOverdue && !isDueSoon)
continue;
// Dedup: don't send more than once per day for the same log
const alreadySent = await prisma_1.prisma.notification.findFirst({
where: {
type: 'VEHICLE_MAINTENANCE_DUE',
companyId: company.id,
createdAt: { gte: oneDayAgo },
data: { path: ['maintenanceLogId'], equals: log.id },
},
});
if (alreadySent)
continue;
// Build human-readable description
const dueParts = [];
if (isOverdueByDate)
dueParts.push(`overdue since ${log.nextDueAt.toLocaleDateString()}`);
else if (dueSoonByDate && daysLeft != null)
dueParts.push(`due in ${daysLeft} day${daysLeft === 1 ? '' : 's'}`);
if (isOverdueByOdometer)
dueParts.push(`overdue by odometer (${Math.abs(kmLeft).toLocaleString()} km ago)`);
else if (dueSoonByOdometer && kmLeft != null)
dueParts.push(`${kmLeft.toLocaleString()} km remaining`);
const title = isOverdue
? `Overdue: ${log.type}${vehicle.make} ${vehicle.model}`
: `${log.type} due soon — ${vehicle.make} ${vehicle.model}`;
const body = `${log.type} for ${vehicle.make} ${vehicle.model} (${vehicle.licensePlate}): ${dueParts.join('; ')}. Please log the service to dismiss this reminder.`;
await prisma_1.prisma.notification.create({
data: {
type: 'VEHICLE_MAINTENANCE_DUE',
title,
body,
data: {
vehicleId: vehicle.id,
maintenanceLogId: log.id,
maintenanceType: log.type,
isOverdue,
daysLeft,
kmLeft,
isOverdueByDate,
isOverdueByOdometer,
},
companyId: company.id,
employeeId: recipient.id,
channel: 'IN_APP',
status: 'DELIVERED',
},
});
}
});
// ─── Start ────────────────────────────────────────────────────
const PORT = Number(process.env.API_PORT ?? 4000);
const HOST = process.env.API_HOST ?? '0.0.0.0';
server.listen(PORT, HOST, () => {
console.log(`[API] Server running on ${HOST}:${PORT}`);
});
//# sourceMappingURL=index.js.map
+1
View File
File diff suppressed because one or more lines are too long
+68
View File
@@ -0,0 +1,68 @@
export type Lang = 'en' | 'fr' | 'ar';
export declare function formatDate(date: Date, lang: Lang, opts?: Intl.DateTimeFormatOptions): string;
export declare const signupEmail: {
subject: (lang: Lang) => string;
text: (opts: {
firstName: string;
companyName: string;
plan: string;
billingPeriod: string;
currency: string;
paymentProvider: string;
trialEnd: Date;
}, lang: Lang) => string;
};
export declare const resetPasswordEmail: {
subject: (lang: Lang) => string;
html: (resetUrl: string, firstName: string, expireMinutes: number, lang: Lang) => string;
text: (resetUrl: string, firstName: string, expireMinutes: number, lang: Lang) => string;
};
export declare const marketplaceReservationEmail: {
subject: (vehicleName: string, lang: Lang) => string;
html: (opts: {
firstName: string;
vehicleYear: number;
vehicleMake: string;
vehicleModel: string;
companyName: string;
startDate: Date;
endDate: Date;
totalDays: number;
rateDisplay: string;
totalDisplay: string;
email: string;
phone?: string;
}, lang: Lang) => string;
text: (opts: {
firstName: string;
vehicleYear: number;
vehicleMake: string;
vehicleModel: string;
companyName: string;
startDate: Date;
endDate: Date;
totalDays: number;
rateDisplay: string;
totalDisplay: string;
}, lang: Lang) => string;
};
export declare const bookingConfirmedNotif: {
title: (lang: Lang) => string;
body: (lang: Lang) => string;
};
export declare const reviewRequestEmail: {
subject: (vehicleLabel: string, lang: Lang) => string;
html: (opts: {
firstName: string;
companyName: string;
vehicleLabel: string;
reviewUrl: string;
}, lang: Lang) => string;
text: (opts: {
firstName: string;
companyName: string;
vehicleLabel: string;
reviewUrl: string;
}, lang: Lang) => string;
};
//# sourceMappingURL=emailTranslations.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"emailTranslations.d.ts","sourceRoot":"","sources":["../../src/lib/emailTranslations.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAQrC,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAE5F;AAID,eAAO,MAAM,WAAW;oBACN,IAAI;iBAMP;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,EAAE,MAAM,CAAA;QACZ,aAAa,EAAE,MAAM,CAAA;QACrB,QAAQ,EAAE,MAAM,CAAA;QAChB,eAAe,EAAE,MAAM,CAAA;QACvB,QAAQ,EAAE,IAAI,CAAA;KACf,QAAQ,IAAI,KAAG,MAAM;CA4CvB,CAAA;AAID,eAAO,MAAM,kBAAkB;oBACb,IAAI;qBAMH,MAAM,aAAa,MAAM,iBAAiB,MAAM,QAAQ,IAAI,KAAG,MAAM;qBA4BrE,MAAM,aAAa,MAAM,iBAAiB,MAAM,QAAQ,IAAI,KAAG,MAAM;CAKvF,CAAA;AAID,eAAO,MAAM,2BAA2B;2BACf,MAAM,QAAQ,IAAI;iBAM5B;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,WAAW,EAAE,MAAM,CAAA;QACnB,SAAS,EAAE,IAAI,CAAA;QACf,OAAO,EAAE,IAAI,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,QAAQ,IAAI,KAAG,MAAM;iBA8CT;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,WAAW,EAAE,MAAM,CAAA;QACnB,SAAS,EAAE,IAAI,CAAA;QACf,OAAO,EAAE,IAAI,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;KACrB,QAAQ,IAAI,KAAG,MAAM;CASvB,CAAA;AAID,eAAO,MAAM,qBAAqB;kBAClB,IAAI;iBAKL,IAAI;CAKlB,CAAA;AAID,eAAO,MAAM,kBAAkB;4BACL,MAAM,QAAQ,IAAI;iBAM7B;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;KAClB,QAAQ,IAAI,KAAG,MAAM;iBAyCT;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;KAClB,QAAQ,IAAI,KAAG,MAAM;CAKvB,CAAA"}
+230
View File
@@ -0,0 +1,230 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reviewRequestEmail = exports.bookingConfirmedNotif = exports.marketplaceReservationEmail = exports.resetPasswordEmail = exports.signupEmail = void 0;
exports.formatDate = formatDate;
function t(map, lang) {
return map[lang] ?? map['fr'];
}
const dateLocale = { en: 'en-GB', fr: 'fr-FR', ar: 'ar-MA' };
function formatDate(date, lang, opts) {
return date.toLocaleDateString(dateLocale[lang], opts ?? { year: 'numeric', month: 'long', day: 'numeric' });
}
// ─── Signup confirmation ──────────────────────────────────────────────────────
exports.signupEmail = {
subject: (lang) => t({
en: 'Your workspace is ready — RentalDriveGo',
fr: 'Votre espace de travail est prêt — RentalDriveGo',
ar: 'مساحة عملك جاهزة — RentalDriveGo',
}, lang),
text: (opts, lang) => {
const trialStr = formatDate(opts.trialEnd, lang);
return t({
en: [
`Hi ${opts.firstName},`,
'',
`Your RentalDriveGo workspace for ${opts.companyName} has been created successfully.`,
`Plan: ${opts.plan} (${opts.billingPeriod.toLowerCase()})`,
`Currency: ${opts.currency}`,
`Primary payment provider: ${opts.paymentProvider}`,
`Free trial ends on ${trialStr}.`,
'',
'Your workspace is ready. Sign in with the email and password you chose during signup.',
'',
'RentalDriveGo',
].join('\n'),
fr: [
`Bonjour ${opts.firstName},`,
'',
`Votre espace de travail RentalDriveGo pour ${opts.companyName} a été créé avec succès.`,
`Forfait : ${opts.plan} (${opts.billingPeriod === 'MONTHLY' ? 'mensuel' : 'annuel'})`,
`Devise : ${opts.currency}`,
`Fournisseur de paiement principal : ${opts.paymentProvider}`,
`La période d'essai gratuit se termine le ${trialStr}.`,
'',
"Votre espace de travail est prêt. Connectez-vous avec l'e-mail et le mot de passe choisis lors de l'inscription.",
'',
'RentalDriveGo',
].join('\n'),
ar: [
`مرحباً ${opts.firstName}،`,
'',
`تم إنشاء مساحة عمل RentalDriveGo الخاصة بـ ${opts.companyName} بنجاح.`,
`الخطة: ${opts.plan} (${opts.billingPeriod === 'MONTHLY' ? 'شهري' : 'سنوي'})`,
`العملة: ${opts.currency}`,
`مزود الدفع الرئيسي: ${opts.paymentProvider}`,
`تنتهي الفترة التجريبية المجانية في ${trialStr}.`,
'',
'مساحة عملك جاهزة. سجّل الدخول باستخدام البريد الإلكتروني وكلمة المرور التي اخترتهما عند التسجيل.',
'',
'RentalDriveGo',
].join('\n'),
}, lang);
},
};
// ─── Password reset ───────────────────────────────────────────────────────────
exports.resetPasswordEmail = {
subject: (lang) => t({
en: 'Reset your RentalDriveGo password',
fr: 'Réinitialisez votre mot de passe RentalDriveGo',
ar: 'إعادة تعيين كلمة مرور RentalDriveGo',
}, lang),
html: (resetUrl, firstName, expireMinutes, lang) => {
const isRtl = lang === 'ar';
const dir = isRtl ? 'rtl' : 'ltr';
return t({
en: `<div dir="ltr" style="font-family:sans-serif;max-width:560px;margin:auto;padding:32px;color:#1c1917">
<p>Hi ${firstName},</p>
<p>You requested a password reset for your RentalDriveGo workspace account.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Reset your password</a></p>
<p>This link expires in ${expireMinutes} minutes. If you did not request this, you can safely ignore this email.</p>
<p>RentalDriveGo</p>
</div>`,
fr: `<div dir="ltr" style="font-family:sans-serif;max-width:560px;margin:auto;padding:32px;color:#1c1917">
<p>Bonjour ${firstName},</p>
<p>Vous avez demandé la réinitialisation du mot de passe de votre compte RentalDriveGo.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Réinitialiser mon mot de passe</a></p>
<p>Ce lien expire dans ${expireMinutes} minutes. Si vous n'avez pas fait cette demande, ignorez simplement cet e-mail.</p>
<p>RentalDriveGo</p>
</div>`,
ar: `<div dir="rtl" style="font-family:sans-serif;max-width:560px;margin:auto;padding:32px;color:#1c1917">
<p>مرحباً ${firstName}،</p>
<p>طلبت إعادة تعيين كلمة مرور حساب RentalDriveGo الخاص بك.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">إعادة تعيين كلمة المرور</a></p>
<p>تنتهي صلاحية هذا الرابط خلال ${expireMinutes} دقيقة. إذا لم تطلب ذلك، يمكنك تجاهل هذا البريد الإلكتروني بأمان.</p>
<p>RentalDriveGo</p>
</div>`,
}, lang);
},
text: (resetUrl, firstName, expireMinutes, lang) => t({
en: `Hi ${firstName},\n\nReset your password here: ${resetUrl}\n\nThis link expires in ${expireMinutes} minutes.\n\nRentalDriveGo`,
fr: `Bonjour ${firstName},\n\nRéinitialisez votre mot de passe ici : ${resetUrl}\n\nCe lien expire dans ${expireMinutes} minutes.\n\nRentalDriveGo`,
ar: `مرحباً ${firstName}،\n\nأعد تعيين كلمة مرورك هنا: ${resetUrl}\n\nينتهي هذا الرابط خلال ${expireMinutes} دقيقة.\n\nRentalDriveGo`,
}, lang),
};
// ─── Marketplace reservation request ─────────────────────────────────────────
exports.marketplaceReservationEmail = {
subject: (vehicleName, lang) => t({
en: `Reservation Request Received — ${vehicleName}`,
fr: `Demande de réservation reçue — ${vehicleName}`,
ar: `تم استلام طلب الحجز — ${vehicleName}`,
}, lang),
html: (opts, lang) => {
const startStr = formatDate(opts.startDate, lang);
const endStr = formatDate(opts.endDate, lang);
const isRtl = lang === 'ar';
const l = t({
en: {
greeting: `Hi ${opts.firstName},`,
intro: 'Your reservation request has been received and is pending company confirmation.',
company: 'Company', pickup: 'Pick-up date', returnD: 'Return date',
duration: 'Duration', daily: 'Daily rate', total: 'Estimated total', days: 'day(s)',
footer: `The company will review your request and get in touch shortly. You may be contacted at ${opts.email}${opts.phone ? ` or ${opts.phone}` : ''}.`,
},
fr: {
greeting: `Bonjour ${opts.firstName},`,
intro: 'Votre demande de réservation a été reçue et est en attente de confirmation.',
company: 'Société', pickup: 'Date de prise en charge', returnD: 'Date de retour',
duration: 'Durée', daily: 'Tarif journalier', total: 'Total estimé', days: 'jour(s)',
footer: `La société examinera votre demande et vous contactera prochainement. Vous pouvez être contacté à ${opts.email}${opts.phone ? ` ou ${opts.phone}` : ''}.`,
},
ar: {
greeting: `مرحباً ${opts.firstName}،`,
intro: 'تم استلام طلب الحجز وهو في انتظار تأكيد الشركة.',
company: 'الشركة', pickup: 'تاريخ الاستلام', returnD: 'تاريخ الإرجاع',
duration: 'المدة', daily: 'السعر اليومي', total: 'الإجمالي التقديري', days: 'يوم',
footer: `ستراجع الشركة طلبك وستتواصل معك قريباً. يمكن التواصل معك على ${opts.email}${opts.phone ? ` أو ${opts.phone}` : ''}.`,
},
}, lang);
return `
<div style="font-family:sans-serif;max-width:560px;margin:auto;padding:32px;color:#1c1917;direction:${isRtl ? 'rtl' : 'ltr'}">
<h2 style="margin-bottom:4px">${l.greeting}</h2>
<p style="color:#57534e">${l.intro}</p>
<hr style="border:none;border-top:1px solid #e7e5e4;margin:24px 0"/>
<h3 style="margin-bottom:12px">${opts.vehicleYear} ${opts.vehicleMake} ${opts.vehicleModel}</h3>
<p><strong>${l.company}:</strong> ${opts.companyName}</p>
<p><strong>${l.pickup}:</strong> ${startStr}</p>
<p><strong>${l.returnD}:</strong> ${endStr}</p>
<p><strong>${l.duration}:</strong> ${opts.totalDays} ${l.days}</p>
<p><strong>${l.daily}:</strong> ${opts.rateDisplay} MAD</p>
<p><strong>${l.total}:</strong> ${opts.totalDisplay} MAD</p>
<hr style="border:none;border-top:1px solid #e7e5e4;margin:24px 0"/>
<p style="color:#57534e;font-size:13px">${l.footer}</p>
</div>
`;
},
text: (opts, lang) => {
const startStr = formatDate(opts.startDate, lang);
const endStr = formatDate(opts.endDate, lang);
return t({
en: `Hi ${opts.firstName},\n\nYour reservation request for the ${opts.vehicleYear} ${opts.vehicleMake} ${opts.vehicleModel} from ${opts.companyName} has been received.\n\nDates: ${startStr}${endStr}\nDuration: ${opts.totalDays} day(s)\nDaily rate: ${opts.rateDisplay} MAD\nEstimated total: ${opts.totalDisplay} MAD\n\nThe company will confirm your request shortly.\n\nThank you!`,
fr: `Bonjour ${opts.firstName},\n\nVotre demande de réservation pour la ${opts.vehicleYear} ${opts.vehicleMake} ${opts.vehicleModel} auprès de ${opts.companyName} a été reçue.\n\nDates : ${startStr}${endStr}\nDurée : ${opts.totalDays} jour(s)\nTarif journalier : ${opts.rateDisplay} MAD\nTotal estimé : ${opts.totalDisplay} MAD\n\nLa société confirmera votre demande prochainement.\n\nMerci !`,
ar: `مرحباً ${opts.firstName}،\n\nتم استلام طلب حجزك لسيارة ${opts.vehicleYear} ${opts.vehicleMake} ${opts.vehicleModel} من ${opts.companyName}.\n\nالتواريخ: ${startStr}${endStr}\nالمدة: ${opts.totalDays} يوم\nالسعر اليومي: ${opts.rateDisplay} MAD\nالإجمالي التقديري: ${opts.totalDisplay} MAD\n\nستؤكد الشركة طلبك قريباً.\n\nشكراً!`,
}, lang);
},
};
// ─── Booking confirmed ────────────────────────────────────────────────────────
exports.bookingConfirmedNotif = {
title: (lang) => t({
en: 'Booking Confirmed',
fr: 'Réservation confirmée',
ar: 'تم تأكيد الحجز',
}, lang),
body: (lang) => t({
en: 'Your booking has been confirmed.',
fr: 'Votre réservation a été confirmée.',
ar: 'تم تأكيد حجزك.',
}, lang),
};
// ─── Post-rental review request ───────────────────────────────────────────────
exports.reviewRequestEmail = {
subject: (vehicleLabel, lang) => t({
en: `How was your rental? — ${vehicleLabel}`,
fr: `Comment s'est passée votre location ? — ${vehicleLabel}`,
ar: `كيف كانت تجربة الإيجار؟ — ${vehicleLabel}`,
}, lang),
html: (opts, lang) => {
const isRtl = lang === 'ar';
const l = t({
en: {
greeting: `Hi ${opts.firstName},`,
body1: `Thank you for renting with <strong>${opts.companyName}</strong>. We hope you enjoyed your <strong>${opts.vehicleLabel}</strong>.`,
body2: 'Your feedback helps the company improve and helps other customers make informed choices. It only takes 30 seconds.',
cta: 'Leave a review',
disclaimer: 'This link is unique to your reservation and can only be used once. If you did not rent a vehicle recently, you can ignore this email.',
},
fr: {
greeting: `Bonjour ${opts.firstName},`,
body1: `Merci d'avoir loué chez <strong>${opts.companyName}</strong>. Nous espérons que vous avez apprécié votre <strong>${opts.vehicleLabel}</strong>.`,
body2: "Votre avis aide la société à s'améliorer et aide les autres clients à faire des choix éclairés. Cela ne prend que 30 secondes.",
cta: 'Laisser un avis',
disclaimer: "Ce lien est unique à votre réservation et ne peut être utilisé qu'une seule fois. Si vous n'avez pas loué de véhicule récemment, vous pouvez ignorer cet e-mail.",
},
ar: {
greeting: `مرحباً ${opts.firstName}،`,
body1: `شكراً لاختيار <strong>${opts.companyName}</strong>. نأمل أنك استمتعت بـ <strong>${opts.vehicleLabel}</strong>.`,
body2: 'تساعد ملاحظاتك الشركة على التحسين وتساعد العملاء الآخرين على اتخاذ قرارات مستنيرة. لا يستغرق الأمر سوى 30 ثانية.',
cta: 'اترك تقييماً',
disclaimer: 'هذا الرابط فريد لحجزك ولا يمكن استخدامه إلا مرة واحدة. إذا لم تستأجر مركبة مؤخراً، يمكنك تجاهل هذا البريد الإلكتروني.',
},
}, lang);
return `
<div style="font-family:sans-serif;max-width:560px;margin:auto;padding:32px;color:#1c1917;direction:${isRtl ? 'rtl' : 'ltr'}">
<h2 style="margin-bottom:4px">${l.greeting}</h2>
<p style="color:#57534e">${l.body1}</p>
<p style="color:#57534e">${l.body2}</p>
<div style="margin:32px 0;text-align:center">
<a href="${opts.reviewUrl}" style="background:#1c1917;color:#ffffff;padding:14px 32px;border-radius:999px;text-decoration:none;font-weight:600;font-size:15px;display:inline-block">
${l.cta}
</a>
</div>
<p style="color:#a8a29e;font-size:12px">${l.disclaimer}</p>
</div>
`;
},
text: (opts, lang) => t({
en: `Hi ${opts.firstName},\n\nThank you for renting with ${opts.companyName}. We hope you enjoyed your ${opts.vehicleLabel}.\n\nLeave a review here (one-time link):\n${opts.reviewUrl}\n\nThank you!`,
fr: `Bonjour ${opts.firstName},\n\nMerci d'avoir loué chez ${opts.companyName}. Nous espérons que vous avez apprécié votre ${opts.vehicleLabel}.\n\nLaissez un avis ici (lien unique) :\n${opts.reviewUrl}\n\nMerci !`,
ar: `مرحباً ${opts.firstName}،\n\nشكراً لاختيار ${opts.companyName}. نأمل أنك استمتعت بـ ${opts.vehicleLabel}.\n\nاترك تقييماً هنا (رابط فريد):\n${opts.reviewUrl}\n\nشكراً!`,
}, lang),
};
//# sourceMappingURL=emailTranslations.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export declare function isDatabaseUnavailableError(error: unknown): boolean;
//# sourceMappingURL=isDatabaseUnavailable.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"isDatabaseUnavailable.d.ts","sourceRoot":"","sources":["../../src/lib/isDatabaseUnavailable.ts"],"names":[],"mappings":"AAAA,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIlE"}
+10
View File
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isDatabaseUnavailableError = isDatabaseUnavailableError;
function isDatabaseUnavailableError(error) {
if (!error || typeof error !== 'object')
return false;
const candidate = error;
return candidate.code === 'P1001' || candidate.message?.includes("Can't reach database server") === true;
}
//# sourceMappingURL=isDatabaseUnavailable.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"isDatabaseUnavailable.js","sourceRoot":"","sources":["../../src/lib/isDatabaseUnavailable.ts"],"names":[],"mappings":";;AAAA,gEAIC;AAJD,SAAgB,0BAA0B,CAAC,KAAc;IACvD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACrD,MAAM,SAAS,GAAG,KAA4C,CAAA;IAC9D,OAAO,SAAS,CAAC,IAAI,KAAK,OAAO,IAAI,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,6BAA6B,CAAC,KAAK,IAAI,CAAA;AAC1G,CAAC"}
+2
View File
@@ -0,0 +1,2 @@
export declare const prisma: any;
//# sourceMappingURL=prisma.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/lib/prisma.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,KAIf,CAAA"}
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.prisma = void 0;
const client_1 = require("@rentaldrivego/database/client");
const globalForPrisma = global;
exports.prisma = globalForPrisma.prisma ??
new client_1.PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = exports.prisma;
}
//# sourceMappingURL=prisma.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["../../src/lib/prisma.ts"],"names":[],"mappings":";;;AAAA,2DAA6D;AAE7D,MAAM,eAAe,GAAG,MAA6C,CAAA;AAExD,QAAA,MAAM,GACjB,eAAe,CAAC,MAAM;IACtB,IAAI,qBAAY,CAAC;QACf,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;KACrF,CAAC,CAAA;AAEJ,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,eAAe,CAAC,MAAM,GAAG,cAAM,CAAA;AACjC,CAAC"}
+3
View File
@@ -0,0 +1,3 @@
import Redis from 'ioredis';
export declare const redis: Redis;
//# sourceMappingURL=redis.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/lib/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAA;AAE3B,eAAO,MAAM,KAAK,OAGhB,CAAA"}
+15
View File
@@ -0,0 +1,15 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.redis = void 0;
const ioredis_1 = __importDefault(require("ioredis"));
exports.redis = new ioredis_1.default(process.env.REDIS_URL ?? 'redis://localhost:6379', {
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000),
});
exports.redis.on('error', (err) => {
console.error('[Redis] Connection error:', err.message);
});
//# sourceMappingURL=redis.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/lib/redis.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA2B;AAEd,QAAA,KAAK,GAAG,IAAI,iBAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,wBAAwB,EAAE;IAChF,oBAAoB,EAAE,CAAC;IACvB,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,EAAE,IAAI,CAAC;CACrD,CAAC,CAAA;AAEF,aAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;IACxB,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;AACzD,CAAC,CAAC,CAAA"}
+11
View File
@@ -0,0 +1,11 @@
export type StorageVisibility = 'public' | 'private';
export declare function getStorageRoot(): string;
export declare function getPublicStorageRoot(): string;
export declare function getPrivateStorageRoot(): string;
export declare function getLegacyStorageRoots(): string[];
export declare function assertStorageConfiguration(): string;
export declare function getProtectedCustomerLicenseImageUrl(customerId: string): string;
export declare function uploadImage(buffer: Buffer, folder: string, publicId?: string, visibility?: StorageVisibility): Promise<string>;
export declare function resolveStoredFilePath(imageUrl: string): string | null;
export declare function deleteImage(imageUrl: string): Promise<void>;
//# sourceMappingURL=storage.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/lib/storage.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEpD,wBAAgB,cAAc,IAAI,MAAM,CAIvC;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAOD,wBAAgB,0BAA0B,IAAI,MAAM,CAkBnD;AAuCD,wBAAgB,mCAAmC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,EACjB,UAAU,GAAE,iBAA2C,GACtD,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcrE;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjE"}
+131
View File
@@ -0,0 +1,131 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getStorageRoot = getStorageRoot;
exports.getPublicStorageRoot = getPublicStorageRoot;
exports.getPrivateStorageRoot = getPrivateStorageRoot;
exports.getLegacyStorageRoots = getLegacyStorageRoots;
exports.assertStorageConfiguration = assertStorageConfiguration;
exports.getProtectedCustomerLicenseImageUrl = getProtectedCustomerLicenseImageUrl;
exports.uploadImage = uploadImage;
exports.resolveStoredFilePath = resolveStoredFilePath;
exports.deleteImage = deleteImage;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const crypto_1 = __importDefault(require("crypto"));
const APP_PACKAGE_ROOT = path_1.default.resolve(__dirname, '..', '..', '..');
const APP_SOURCE_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'src');
const APP_DIST_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'dist');
const DEFAULT_STORAGE_ROOT = path_1.default.join(APP_PACKAGE_ROOT, 'storage');
const LEGACY_STORAGE_ROOTS = [
path_1.default.join(APP_SOURCE_ROOT, 'lib', 'storage'),
];
function getStorageRoot() {
return process.env.FILE_STORAGE_ROOT
? path_1.default.resolve(process.env.FILE_STORAGE_ROOT)
: DEFAULT_STORAGE_ROOT;
}
function getPublicStorageRoot() {
return path_1.default.join(getStorageRoot(), 'public');
}
function getPrivateStorageRoot() {
return path_1.default.join(getStorageRoot(), 'private');
}
function getLegacyStorageRoots() {
return LEGACY_STORAGE_ROOTS;
}
function isWithinPath(targetPath, parentPath) {
const relative = path_1.default.relative(parentPath, targetPath);
return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
}
function assertStorageConfiguration() {
const storageRoot = getStorageRoot();
if (process.env.NODE_ENV === 'production' && !process.env.FILE_STORAGE_ROOT) {
throw new Error('FILE_STORAGE_ROOT must be set in production so uploads are stored on the mounted volume.');
}
if (process.env.NODE_ENV === 'production') {
const forbiddenRoots = [APP_PACKAGE_ROOT, APP_SOURCE_ROOT, APP_DIST_ROOT];
const invalidRoot = forbiddenRoots.find((root) => isWithinPath(storageRoot, root));
if (invalidRoot) {
throw new Error(`FILE_STORAGE_ROOT must point outside the API app tree in production. Received ${storageRoot}, which is inside ${invalidRoot}.`);
}
}
return storageRoot;
}
function ensureStorageRoot(visibility) {
assertStorageConfiguration();
const root = visibility === 'public' ? getPublicStorageRoot() : getPrivateStorageRoot();
fs_1.default.mkdirSync(root, { recursive: true });
return root;
}
function inferVisibility(folder) {
const normalized = folder.replace(/\\/g, '/');
if (/\/customers\//.test(`/${normalized}/`))
return 'private';
if (/\/licenses?\//.test(`/${normalized}/`))
return 'private';
if (/\/contracts?\//.test(`/${normalized}/`))
return 'private';
if (/\/documents?\//.test(`/${normalized}/`))
return 'private';
if (/\/inspections?\//.test(`/${normalized}/`))
return 'private';
if (/\/internal\//.test(`/${normalized}/`))
return 'private';
return 'public';
}
function getApiBase() {
return (process.env.API_URL ?? 'http://localhost:4000').replace(/\/$/, '');
}
function getDashboardBase() {
return (process.env.DASHBOARD_URL ??
process.env.NEXT_PUBLIC_DASHBOARD_URL ??
'http://localhost:3000/dashboard').replace(/\/$/, '');
}
function getStorageRelativePath(imageUrl) {
const marker = '/storage/';
const idx = imageUrl.indexOf(marker);
if (idx === -1)
return null;
return imageUrl.slice(idx + marker.length);
}
function getProtectedCustomerLicenseImageUrl(customerId) {
return `${getDashboardBase()}/api/v1/customers/${customerId}/license-image`;
}
async function uploadImage(buffer, folder, publicId, visibility = inferVisibility(folder)) {
const storageRoot = ensureStorageRoot(visibility);
const folderPath = path_1.default.join(storageRoot, folder);
if (!isWithinPath(folderPath, storageRoot)) {
throw new Error('Upload path escapes storage root');
}
fs_1.default.mkdirSync(folderPath, { recursive: true });
const filename = publicId
? `${publicId}.jpg`
: `${crypto_1.default.randomBytes(16).toString('hex')}.jpg`;
fs_1.default.writeFileSync(path_1.default.join(folderPath, filename), buffer);
return `${getApiBase()}/storage/${folder}/${filename}`;
}
function resolveStoredFilePath(imageUrl) {
const relative = getStorageRelativePath(imageUrl);
if (!relative)
return null;
const roots = [getPublicStorageRoot(), getPrivateStorageRoot(), ...getLegacyStorageRoots(), assertStorageConfiguration()];
for (const root of roots) {
const filePath = path_1.default.join(root, relative);
if (!isWithinPath(filePath, root))
continue;
if (fs_1.default.existsSync(filePath)) {
return filePath;
}
}
return null;
}
async function deleteImage(imageUrl) {
const filePath = resolveStoredFilePath(imageUrl);
if (filePath && fs_1.default.existsSync(filePath)) {
fs_1.default.unlinkSync(filePath);
}
}
//# sourceMappingURL=storage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/lib/storage.ts"],"names":[],"mappings":"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"}
+13
View File
@@ -0,0 +1,13 @@
import { Request, Response } from 'express';
/**
* Sends a uniform auth-error JSON response.
* All auth middleware must go through this function so the shape is identical
* to what the errorMiddleware produces for AppError instances.
*/
export declare function sendUnauthorized(res: Response, error: string, message: string): Response<any, Record<string, any>>;
export declare function getCookie(req: Request, name: string): string | null;
export declare function getBearerToken(req: Request): string | null;
export declare function getAuthToken(req: Request, cookieName?: string): string | null;
export declare function sendForbidden(res: Response, error: string, message: string, extra?: Record<string, unknown>): Response<any, Record<string, any>>;
export declare function sendPaymentRequired(res: Response, error: string, message: string, extra?: Record<string, unknown>): Response<any, Record<string, any>>;
//# sourceMappingURL=authHelpers.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"authHelpers.d.ts","sourceRoot":"","sources":["../../src/middleware/authHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAE3C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,sCAE7E;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYnE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK1D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7E;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,sCAE3G;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,sCAEjH"}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendUnauthorized = sendUnauthorized;
exports.getCookie = getCookie;
exports.getBearerToken = getBearerToken;
exports.getAuthToken = getAuthToken;
exports.sendForbidden = sendForbidden;
exports.sendPaymentRequired = sendPaymentRequired;
/**
* Sends a uniform auth-error JSON response.
* All auth middleware must go through this function so the shape is identical
* to what the errorMiddleware produces for AppError instances.
*/
function sendUnauthorized(res, error, message) {
return res.status(401).json({ error, message, statusCode: 401 });
}
function getCookie(req, name) {
const cookieHeader = req.headers.cookie;
if (!cookieHeader)
return null;
for (const chunk of cookieHeader.split(';')) {
const [rawName, ...rawValue] = chunk.trim().split('=');
if (rawName === name) {
return decodeURIComponent(rawValue.join('='));
}
}
return null;
}
function getBearerToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer '))
return null;
const token = authHeader.slice(7).trim();
return token.length > 0 ? token : null;
}
function getAuthToken(req, cookieName) {
// Prefer HttpOnly cookies over bearer tokens so stale script-readable tokens
// cannot shadow a valid server-managed session during migration.
return (cookieName ? getCookie(req, cookieName) : null) ?? getBearerToken(req);
}
function sendForbidden(res, error, message, extra) {
return res.status(403).json({ error, message, statusCode: 403, ...extra });
}
function sendPaymentRequired(res, error, message, extra) {
return res.status(402).json({ error, message, statusCode: 402, ...extra });
}
//# sourceMappingURL=authHelpers.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"authHelpers.js","sourceRoot":"","sources":["../../src/middleware/authHelpers.ts"],"names":[],"mappings":";;AAOA,4CAEC;AAED,8BAYC;AAED,wCAKC;AAED,oCAIC;AAED,sCAEC;AAED,kDAEC;AA1CD;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,GAAa,EAAE,KAAa,EAAE,OAAe;IAC5E,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;AAClE,CAAC;AAED,SAAgB,SAAS,CAAC,GAAY,EAAE,IAAY;IAClD,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAA;IACvC,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAA;IAE9B,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACtD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,OAAO,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAC/C,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY;IACzC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAA;IAC5C,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IACnD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACxC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACxC,CAAC;AAED,SAAgB,YAAY,CAAC,GAAY,EAAE,UAAmB;IAC5D,6EAA6E;IAC7E,iEAAiE;IACjE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAA;AAChF,CAAC;AAED,SAAgB,aAAa,CAAC,GAAa,EAAE,KAAa,EAAE,OAAe,EAAE,KAA+B;IAC1G,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;AAC5E,CAAC;AAED,SAAgB,mBAAmB,CAAC,GAAa,EAAE,KAAa,EAAE,OAAe,EAAE,KAA+B;IAChH,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;AAC5E,CAAC"}
+6
View File
@@ -0,0 +1,6 @@
export declare const authLimiter: import("express-rate-limit").RateLimitRequestHandler;
export declare const apiLimiter: import("express-rate-limit").RateLimitRequestHandler;
export declare const publicLimiter: import("express-rate-limit").RateLimitRequestHandler;
export declare const adminLimiter: import("express-rate-limit").RateLimitRequestHandler;
export declare const actorLimiter: import("express-rate-limit").RateLimitRequestHandler;
//# sourceMappingURL=rateLimiter.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"rateLimiter.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimiter.ts"],"names":[],"mappings":"AA0DA,eAAO,MAAM,WAAW,sDAQtB,CAAA;AAGF,eAAO,MAAM,UAAU,sDAarB,CAAA;AAGF,eAAO,MAAM,aAAa,sDAOxB,CAAA;AAGF,eAAO,MAAM,YAAY,sDAUvB,CAAA;AAKF,eAAO,MAAM,YAAY,sDAevB,CAAA"}
+151
View File
@@ -0,0 +1,151 @@
"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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.actorLimiter = exports.adminLimiter = exports.publicLimiter = exports.apiLimiter = exports.authLimiter = void 0;
const express_rate_limit_1 = __importStar(require("express-rate-limit"));
const tokens_1 = require("../security/tokens");
const sessionCookies_1 = require("../security/sessionCookies");
const SESSION_COOKIE_NAMES = [
(0, sessionCookies_1.getSessionCookieName)('admin'),
(0, sessionCookies_1.getSessionCookieName)('employee'),
(0, sessionCookies_1.getSessionCookieName)('renter'),
];
function readCookie(cookieHeader, name) {
if (!cookieHeader)
return null;
for (const chunk of cookieHeader.split(';')) {
const [rawName, ...rawValue] = chunk.trim().split('=');
if (rawName === name)
return decodeURIComponent(rawValue.join('='));
}
return null;
}
function getBearerToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer '))
return null;
const token = authHeader.slice(7).trim();
return token || null;
}
function getRequestToken(req) {
const cookieHeader = req.headers.cookie;
for (const name of SESSION_COOKIE_NAMES) {
const token = readCookie(cookieHeader, name);
if (token)
return token;
}
return getBearerToken(req);
}
function getAuthenticatedActorKey(req) {
const token = getRequestToken(req);
if (!token)
return null;
try {
const payload = (0, tokens_1.verifyAnyActorToken)(token);
return `${payload.type}:${payload.sub}`;
}
catch {
return null;
}
}
// req.ip is already the real client IP when app.set('trust proxy', 1) is configured
const getClientIpKey = (req) => (0, express_rate_limit_1.ipKeyGenerator)(req.ip ?? '');
// Strict limiter for auth endpoints — prevents brute-force and credential stuffing.
// Successful requests (e.g. GET /me profile reads) are skipped so only failed
// attempts count toward the cap.
exports.authLimiter = (0, express_rate_limit_1.default)({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20,
standardHeaders: 'draft-7',
legacyHeaders: false,
skipSuccessfulRequests: true,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: 'Too many attempts, please try again later', statusCode: 429 },
});
// Standard limiter for general authenticated API endpoints
exports.apiLimiter = (0, express_rate_limit_1.default)({
windowMs: 60 * 1000, // 1 minute
max: 120,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => {
const ip = getClientIpKey(req);
const actorKey = getAuthenticatedActorKey(req);
const companyId = req.companyId ?? '';
const renterId = req.renterId ?? '';
return `${ip}:${actorKey || companyId || renterId || 'anonymous'}`;
},
message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 },
});
// Limiter for public marketplace and site endpoints (no auth)
exports.publicLimiter = (0, express_rate_limit_1.default)({
windowMs: 60 * 1000,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 },
});
// Tight limiter for admin endpoints
exports.adminLimiter = (0, express_rate_limit_1.default)({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => {
const ip = getClientIpKey(req);
return `${ip}:${getAuthenticatedActorKey(req) || 'anonymous'}`;
},
message: { error: 'too_many_requests', message: 'Too many admin requests', statusCode: 429 },
});
// Applied after authentication so limits can include actor identity rather than
// pretending every employee behind the same NAT is the same organism.
exports.actorLimiter = (0, express_rate_limit_1.default)({
windowMs: 60 * 1000,
max: 240,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => {
const ip = getClientIpKey(req);
const actorKey = getAuthenticatedActorKey(req);
const companyId = req.companyId ?? '';
const employeeId = req.employee?.id ?? '';
const renterId = req.renterId ?? '';
const adminId = req.admin?.id ?? '';
return `${ip}:${companyId}:${actorKey || employeeId || renterId || adminId || 'anonymous'}`;
},
message: { error: 'too_many_requests', message: 'Authenticated rate limit exceeded', statusCode: 429 },
});
//# sourceMappingURL=rateLimiter.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"rateLimiter.js","sourceRoot":"","sources":["../../src/middleware/rateLimiter.ts"],"names":[],"mappings":"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"}
+3
View File
@@ -0,0 +1,3 @@
import type { Request, Response, NextFunction } from 'express';
export declare function requestIdMiddleware(req: Request, res: Response, next: NextFunction): void;
//# sourceMappingURL=requestId.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requestId.d.ts","sourceRoot":"","sources":["../../src/middleware/requestId.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE9D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAMlF"}
+15
View File
@@ -0,0 +1,15 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestIdMiddleware = requestIdMiddleware;
const crypto_1 = __importDefault(require("crypto"));
function requestIdMiddleware(req, res, next) {
const incoming = req.headers['x-request-id'];
const requestId = Array.isArray(incoming) ? incoming[0] : incoming;
req.requestId = requestId && requestId.length <= 128 ? requestId : `req_${crypto_1.default.randomUUID()}`;
res.setHeader('X-Request-Id', req.requestId);
next();
}
//# sourceMappingURL=requestId.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requestId.js","sourceRoot":"","sources":["../../src/middleware/requestId.ts"],"names":[],"mappings":";;;;;AAGA,kDAMC;AATD,oDAA2B;AAG3B,SAAgB,mBAAmB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACjF,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAClE,GAAG,CAAC,SAAS,GAAG,SAAS,IAAI,SAAS,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,gBAAM,CAAC,UAAU,EAAE,EAAE,CAAA;IAC/F,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,EAAE,CAAA;AACR,CAAC"}
+16
View File
@@ -0,0 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import { AdminRole } from '@rentaldrivego/database';
/**
* Requires a valid admin session token.
*
* Guarantees on success:
* req.admin — the full AdminUser record
*/
export declare function requireAdminAuth(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
/**
* Requires the authenticated admin to have at least `minimumRole`.
* Must be applied after `requireAdminAuth`.
*/
export declare function requireAdminRole(minimumRole: AdminRole): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
export declare function requireFreshAdmin2FA(req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>> | undefined;
//# sourceMappingURL=requireAdminAuth.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireAdminAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/requireAdminAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEzD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AA0BnD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,2DAwBrF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,SAAS,IAC7C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,oDAaxD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,kDAanF"}
+85
View File
@@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireAdminAuth = requireAdminAuth;
exports.requireAdminRole = requireAdminRole;
exports.requireFreshAdmin2FA = requireFreshAdmin2FA;
const prisma_1 = require("../lib/prisma");
const authHelpers_1 = require("./authHelpers");
const tokens_1 = require("../security/tokens");
const sessionCookies_1 = require("../security/sessionCookies");
const ROLE_RANK = {
SUPER_ADMIN: 5,
ADMIN: 4,
SUPPORT: 3,
FINANCE: 2,
VIEWER: 1,
};
const ADMIN_2FA_ENROLLMENT_EXEMPT_PATHS = new Set([
'/auth/me',
'/auth/logout',
'/auth/2fa/setup',
'/auth/2fa/verify',
]);
const FRESH_2FA_WINDOW_MS = Number(process.env.ADMIN_FRESH_2FA_WINDOW_MS ?? 10 * 60 * 1000);
function is2faEnrollmentExempt(req) {
return ADMIN_2FA_ENROLLMENT_EXEMPT_PATHS.has(req.path);
}
/**
* Requires a valid admin session token.
*
* Guarantees on success:
* req.admin — the full AdminUser record
*/
async function requireAdminAuth(req, res, next) {
const token = (0, authHelpers_1.getAuthToken)(req, (0, sessionCookies_1.getSessionCookieName)('admin'));
if (!token)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Admin authentication required');
let payload;
try {
payload = (0, tokens_1.verifyActorToken)(token, 'admin');
}
catch {
return (0, authHelpers_1.sendUnauthorized)(res, 'invalid_token', 'Invalid or expired admin token');
}
const admin = await prisma_1.prisma.adminUser.findUnique({ where: { id: payload.sub } });
if (!admin || !admin.isActive) {
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Admin account not found or deactivated');
}
if (!admin.totpEnabled && !is2faEnrollmentExempt(req)) {
return (0, authHelpers_1.sendForbidden)(res, 'admin_2fa_required', 'Admin 2FA enrollment is required before using privileged admin routes');
}
req.admin = admin;
req.adminAuthLast2faAt = typeof payload.last2faAt === 'number' ? payload.last2faAt : undefined;
next();
}
/**
* Requires the authenticated admin to have at least `minimumRole`.
* Must be applied after `requireAdminAuth`.
*/
function requireAdminRole(minimumRole) {
return (req, res, next) => {
const admin = req.admin;
if (!admin)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Admin authentication required');
const rank = ROLE_RANK[admin.role] ?? 0;
const required = ROLE_RANK[minimumRole] ?? 99;
if (rank < required) {
return (0, authHelpers_1.sendForbidden)(res, 'forbidden', `This action requires the ${minimumRole} role or higher`);
}
next();
};
}
function requireFreshAdmin2FA(req, res, next) {
const admin = req.admin;
if (!admin)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Admin authentication required');
if (!admin.totpEnabled) {
return (0, authHelpers_1.sendForbidden)(res, 'admin_2fa_required', 'Admin 2FA enrollment is required for this action');
}
const last2faAt = req.adminAuthLast2faAt;
if (!last2faAt || Date.now() - last2faAt > FRESH_2FA_WINDOW_MS) {
return (0, authHelpers_1.sendForbidden)(res, 'fresh_2fa_required', 'Fresh admin 2FA verification is required for this action');
}
next();
}
//# sourceMappingURL=requireAdminAuth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireAdminAuth.js","sourceRoot":"","sources":["../../src/middleware/requireAdminAuth.ts"],"names":[],"mappings":";;AAkCA,4CAwBC;AAMD,4CAcC;AAED,oDAaC;AA5FD,0CAAsC;AAEtC,+CAA6E;AAC7E,+CAAqD;AACrD,+DAAiE;AAEjE,MAAM,SAAS,GAA8B;IAC3C,WAAW,EAAE,CAAC;IACd,KAAK,EAAQ,CAAC;IACd,OAAO,EAAM,CAAC;IACd,OAAO,EAAM,CAAC;IACd,MAAM,EAAO,CAAC;CACf,CAAA;AAED,MAAM,iCAAiC,GAAG,IAAI,GAAG,CAAC;IAChD,UAAU;IACV,cAAc;IACd,iBAAiB;IACjB,kBAAkB;CACnB,CAAC,CAAA;AAEF,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AAE3F,SAAS,qBAAqB,CAAC,GAAY;IACzC,OAAO,iCAAiC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACxD,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,gBAAgB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACpF,MAAM,KAAK,GAAG,IAAA,0BAAY,EAAC,GAAG,EAAE,IAAA,qCAAoB,EAAC,OAAO,CAAC,CAAC,CAAA;IAE9D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,+BAA+B,CAAC,CAAA;IAE5F,IAAI,OAA0D,CAAA;IAC9D,IAAI,CAAC;QACH,OAAO,GAAG,IAAA,yBAAgB,EAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,eAAe,EAAE,gCAAgC,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,eAAM,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC/E,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC9B,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,wCAAwC,CAAC,CAAA;IAC3F,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC;QACtD,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,oBAAoB,EAAE,uEAAuE,CAAC,CAAA;IAC1H,CAAC;IAED,GAAG,CAAC,KAAK,GAAG,KAAK,CAAA;IACjB,GAAG,CAAC,kBAAkB,GAAG,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;IAC9F,IAAI,EAAE,CAAA;AACR,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,WAAsB;IACrD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;QACvB,IAAI,CAAC,KAAK;YAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,+BAA+B,CAAC,CAAA;QAE5F,MAAM,IAAI,GAAO,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAM,CAAC,CAAA;QAC7C,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC,IAAK,EAAE,CAAA;QAE9C,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;YACpB,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,WAAW,EAAE,4BAA4B,WAAW,iBAAiB,CAAC,CAAA;QAClG,CAAC;QAED,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;AACH,CAAC;AAED,SAAgB,oBAAoB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAClF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;IACvB,IAAI,CAAC,KAAK;QAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,+BAA+B,CAAC,CAAA;IAC5F,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACvB,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,oBAAoB,EAAE,kDAAkD,CAAC,CAAA;IACrG,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,kBAAkB,CAAA;IACxC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,mBAAmB,EAAE,CAAC;QAC/D,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,oBAAoB,EAAE,0DAA0D,CAAC,CAAA;IAC7G,CAAC;IAED,IAAI,EAAE,CAAA;AACR,CAAC"}
+3
View File
@@ -0,0 +1,3 @@
import { Request, Response, NextFunction } from 'express';
export declare function requireApiKey(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
//# sourceMappingURL=requireApiKey.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireApiKey.d.ts","sourceRoot":"","sources":["../../src/middleware/requireApiKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAIzD,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,2DA8BlF"}
+31
View File
@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireApiKey = requireApiKey;
const prisma_1 = require("../lib/prisma");
const apiKeys_1 = require("../security/apiKeys");
async function requireApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'missing_api_key', message: 'API key required in x-api-key header', statusCode: 401 });
}
const prefix = (0, apiKeys_1.getApiKeyPrefix)(apiKey);
if (!prefix) {
return res.status(401).json({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401 });
}
const keyRecord = await prisma_1.prisma.companyApiKey.findUnique({
where: { prefix },
include: { company: true },
});
const hashed = (0, apiKeys_1.hashApiKey)(apiKey);
if (!keyRecord || keyRecord.revokedAt || !(0, apiKeys_1.timingSafeEqualHex)(hashed, keyRecord.keyHash)) {
return res.status(401).json({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401 });
}
await prisma_1.prisma.companyApiKey.update({
where: { id: keyRecord.id },
data: { lastUsedAt: new Date() },
});
req.company = keyRecord.company;
req.companyId = keyRecord.companyId;
next();
}
//# sourceMappingURL=requireApiKey.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireApiKey.js","sourceRoot":"","sources":["../../src/middleware/requireApiKey.ts"],"names":[],"mappings":";;AAIA,sCA8BC;AAjCD,0CAAsC;AACtC,iDAAqF;AAE9E,KAAK,UAAU,aAAa,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACjF,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,CAAuB,CAAA;IAE7D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,sCAAsC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7H,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,yBAAe,EAAC,MAAM,CAAC,CAAA;IACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;IACxG,CAAC;IAED,MAAM,SAAS,GAAG,MAAO,eAAc,CAAC,aAAa,CAAC,UAAU,CAAC;QAC/D,KAAK,EAAE,EAAE,MAAM,EAAE;QACjB,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;KAC3B,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,IAAA,oBAAU,EAAC,MAAM,CAAC,CAAA;IACjC,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS,IAAI,CAAC,IAAA,4BAAkB,EAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QACxF,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;IACxG,CAAC;IAED,MAAO,eAAc,CAAC,aAAa,CAAC,MAAM,CAAC;QACzC,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE;QAC3B,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE;KACjC,CAAC,CAAA;IAEF,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAA;IAC/B,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC,SAAS,CAAA;IACnC,IAAI,EAAE,CAAA;AACR,CAAC"}
+4
View File
@@ -0,0 +1,4 @@
import { Request, Response, NextFunction } from 'express';
export declare function requireCompanyAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
export declare function requireCompanyDocumentAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
//# sourceMappingURL=requireCompanyAuth.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireCompanyAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/requireCompanyAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AA0CzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,sDAEvF;AAED,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,sDAE/F"}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireCompanyAuth = requireCompanyAuth;
exports.requireCompanyDocumentAuth = requireCompanyDocumentAuth;
const prisma_1 = require("../lib/prisma");
const authHelpers_1 = require("./authHelpers");
const tokens_1 = require("../security/tokens");
const sessionCookies_1 = require("../security/sessionCookies");
const rateLimiter_1 = require("./rateLimiter");
/**
* Validates a company-scoped Bearer token and loads the employee + company onto `req`.
*
* Guarantees on success:
* req.employee — full employee record (with company relation)
* req.company — the employee's company
* req.companyId — string shorthand for req.company.id
*/
async function authenticateCompanyRequest(req, res, next) {
const token = (0, authHelpers_1.getAuthToken)(req, (0, sessionCookies_1.getSessionCookieName)('employee'));
if (!token)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Authentication required');
let payload;
try {
payload = (0, tokens_1.verifyActorToken)(token, 'employee');
}
catch {
return (0, authHelpers_1.sendUnauthorized)(res, 'invalid_token', 'Invalid or expired session token');
}
const employee = await prisma_1.prisma.employee.findUnique({
where: { id: payload.sub },
include: { company: true },
});
if (!employee || !employee.isActive) {
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Employee account not found or inactive');
}
req.employee = employee;
req.company = employee.company;
req.companyId = employee.companyId;
return (0, rateLimiter_1.actorLimiter)(req, res, next);
}
async function requireCompanyAuth(req, res, next) {
return authenticateCompanyRequest(req, res, next);
}
async function requireCompanyDocumentAuth(req, res, next) {
return authenticateCompanyRequest(req, res, next);
}
//# sourceMappingURL=requireCompanyAuth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireCompanyAuth.js","sourceRoot":"","sources":["../../src/middleware/requireCompanyAuth.ts"],"names":[],"mappings":";;AA0CA,gDAEC;AAED,gEAEC;AA/CD,0CAAsC;AACtC,+CAA8D;AAC9D,+CAAqD;AACrD,+DAAiE;AACjE,+CAA4C;AAE5C;;;;;;;GAOG;AACH,KAAK,UAAU,0BAA0B,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACvF,MAAM,KAAK,GAAG,IAAA,0BAAY,EAAC,GAAG,EAAE,IAAA,qCAAoB,EAAC,UAAU,CAAC,CAAC,CAAA;IAEjE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAA;IAEtF,IAAI,OAAsC,CAAA;IAC1C,IAAI,CAAC;QACH,OAAO,GAAG,IAAA,yBAAgB,EAAC,KAAK,EAAE,UAAU,CAAC,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,eAAe,EAAE,kCAAkC,CAAC,CAAA;IACnF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,eAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;QAChD,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE;QAC1B,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;KAC3B,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACpC,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,wCAAwC,CAAC,CAAA;IAC3F,CAAC;IAED,GAAG,CAAC,QAAQ,GAAI,QAAQ,CAAA;IACxB,GAAG,CAAC,OAAO,GAAK,QAAQ,CAAC,OAAO,CAAA;IAChC,GAAG,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAA;IAClC,OAAO,IAAA,0BAAY,EAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACrC,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACtF,OAAO,0BAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACnD,CAAC;AAEM,KAAK,UAAU,0BAA0B,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC9F,OAAO,0BAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACnD,CAAC"}
+4
View File
@@ -0,0 +1,4 @@
import type { Request, Response, NextFunction } from 'express';
import { type CompanyPolicyAction } from '../security/policies/companyPolicy';
export declare function requireCompanyPolicy(action: CompanyPolicyAction): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
//# sourceMappingURL=requireCompanyPolicy.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"requireCompanyPolicy.d.ts","sourceRoot":"","sources":["../../src/middleware/requireCompanyPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAG9D,OAAO,EAAiB,KAAK,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AAE5F,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,mBAAmB,IACtD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,oDAexD"}
+22
View File
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireCompanyPolicy = requireCompanyPolicy;
const authHelpers_1 = require("./authHelpers");
const companyPolicy_1 = require("../security/policies/companyPolicy");
function requireCompanyPolicy(action) {
return (req, res, next) => {
const employee = req.employee;
if (!employee)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Authentication required');
const allowedRoles = companyPolicy_1.CompanyPolicy[action];
if (!allowedRoles.includes(employee.role)) {
return (0, authHelpers_1.sendForbidden)(res, 'forbidden', 'You do not have permission to perform this action', {
action,
allowedRoles,
yourRole: employee.role,
});
}
next();
};
}
//# sourceMappingURL=requireCompanyPolicy.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireCompanyPolicy.js","sourceRoot":"","sources":["../../src/middleware/requireCompanyPolicy.ts"],"names":[],"mappings":";;AAKA,oDAgBC;AApBD,+CAA+D;AAE/D,sEAA4F;AAE5F,SAAgB,oBAAoB,CAAC,MAA2B;IAC9D,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC7B,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAA;QAEzF,MAAM,YAAY,GAA4B,6BAAa,CAAC,MAAM,CAAC,CAAA;QACnE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,WAAW,EAAE,mDAAmD,EAAE;gBAC1F,MAAM;gBACN,YAAY;gBACZ,QAAQ,EAAE,QAAQ,CAAC,IAAI;aACxB,CAAC,CAAA;QACJ,CAAC;QAED,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;AACH,CAAC"}
+17
View File
@@ -0,0 +1,17 @@
import { Request, Response, NextFunction } from 'express';
/**
* Requires a valid renter Bearer token.
*
* Guarantees on success:
* req.renterId — the authenticated renter's id
*/
export declare function requireRenterAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
/**
* Optionally extracts renter identity from a Bearer token.
* Never blocks the request — invalid or absent tokens are silently ignored.
*
* Sets on success:
* req.renterId — the authenticated renter's id (if token is valid)
*/
export declare function optionalRenterAuth(req: Request, _res: Response, next: NextFunction): Promise<void>;
//# sourceMappingURL=requireRenterAuth.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireRenterAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/requireRenterAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAOzD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,sDAmBtF;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,iBAYxF"}
+54
View File
@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireRenterAuth = requireRenterAuth;
exports.optionalRenterAuth = optionalRenterAuth;
const prisma_1 = require("../lib/prisma");
const authHelpers_1 = require("./authHelpers");
const tokens_1 = require("../security/tokens");
const sessionCookies_1 = require("../security/sessionCookies");
const rateLimiter_1 = require("./rateLimiter");
/**
* Requires a valid renter Bearer token.
*
* Guarantees on success:
* req.renterId — the authenticated renter's id
*/
async function requireRenterAuth(req, res, next) {
const token = (0, authHelpers_1.getAuthToken)(req, (0, sessionCookies_1.getSessionCookieName)('renter'));
if (!token)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Renter authentication required');
let payload;
try {
payload = (0, tokens_1.verifyActorToken)(token, 'renter');
}
catch {
return (0, authHelpers_1.sendUnauthorized)(res, 'invalid_token', 'Invalid or expired token');
}
const renter = await prisma_1.prisma.renter.findUnique({ where: { id: payload.sub } });
if (!renter || !renter.isActive) {
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Renter account not found or inactive');
}
req.renterId = renter.id;
return (0, rateLimiter_1.actorLimiter)(req, res, next);
}
/**
* Optionally extracts renter identity from a Bearer token.
* Never blocks the request — invalid or absent tokens are silently ignored.
*
* Sets on success:
* req.renterId — the authenticated renter's id (if token is valid)
*/
async function optionalRenterAuth(req, _res, next) {
const token = (0, authHelpers_1.getAuthToken)(req, (0, sessionCookies_1.getSessionCookieName)('renter'));
if (!token)
return next();
try {
const payload = (0, tokens_1.verifyActorToken)(token, 'renter');
req.renterId = payload.sub;
}
catch {
// Optional — silently ignore invalid tokens
}
next();
}
//# sourceMappingURL=requireRenterAuth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireRenterAuth.js","sourceRoot":"","sources":["../../src/middleware/requireRenterAuth.ts"],"names":[],"mappings":";;AAaA,8CAmBC;AASD,gDAYC;AApDD,0CAAsC;AACtC,+CAA8D;AAC9D,+CAAqD;AACrD,+DAAiE;AACjE,+CAA4C;AAE5C;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,MAAM,KAAK,GAAG,IAAA,0BAAY,EAAC,GAAG,EAAE,IAAA,qCAAoB,EAAC,QAAQ,CAAC,CAAC,CAAA;IAE/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,gCAAgC,CAAC,CAAA;IAE7F,IAAI,OAAsC,CAAA;IAC1C,IAAI,CAAC;QACH,OAAO,GAAG,IAAA,yBAAgB,EAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,eAAe,EAAE,0BAA0B,CAAC,CAAA;IAC3E,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,eAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC7E,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,sCAAsC,CAAC,CAAA;IACzF,CAAC;IAED,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAA;IACxB,OAAO,IAAA,0BAAY,EAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACrC,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,kBAAkB,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IACvF,MAAM,KAAK,GAAG,IAAA,0BAAY,EAAC,GAAG,EAAE,IAAA,qCAAoB,EAAC,QAAQ,CAAC,CAAC,CAAA;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,EAAE,CAAA;IAEzB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAA,yBAAgB,EAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;QACjD,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;IAED,IAAI,EAAE,CAAA;AACR,CAAC"}
+8
View File
@@ -0,0 +1,8 @@
import { Request, Response, NextFunction } from 'express';
import { EmployeeRole } from '@rentaldrivego/database';
/**
* Requires the authenticated employee to have at least `minimumRole`.
* Must be applied after `requireCompanyAuth`.
*/
export declare function requireRole(minimumRole: EmployeeRole): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
//# sourceMappingURL=requireRole.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireRole.d.ts","sourceRoot":"","sources":["../../src/middleware/requireRole.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAStD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,YAAY,IAC3C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,oDAgBxD"}
+30
View File
@@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireRole = requireRole;
const authHelpers_1 = require("./authHelpers");
const ROLE_RANK = {
OWNER: 3,
MANAGER: 2,
AGENT: 1,
};
/**
* Requires the authenticated employee to have at least `minimumRole`.
* Must be applied after `requireCompanyAuth`.
*/
function requireRole(minimumRole) {
return (req, res, next) => {
const employee = req.employee;
if (!employee)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Authentication required');
const employeeRank = ROLE_RANK[employee.role] ?? 0;
const requiredRank = ROLE_RANK[minimumRole] ?? 99;
if (employeeRank < requiredRank) {
return (0, authHelpers_1.sendForbidden)(res, 'forbidden', `This action requires the ${minimumRole} role or higher`, {
requiredRole: minimumRole,
yourRole: employee.role,
});
}
next();
};
}
//# sourceMappingURL=requireRole.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireRole.js","sourceRoot":"","sources":["../../src/middleware/requireRole.ts"],"names":[],"mappings":";;AAcA,kCAiBC;AA7BD,+CAA+D;AAE/D,MAAM,SAAS,GAAiC;IAC9C,KAAK,EAAI,CAAC;IACV,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,CAAC;CACX,CAAA;AAED;;;GAGG;AACH,SAAgB,WAAW,CAAC,WAAyB;IACnD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC7B,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAA;QAEzF,MAAM,YAAY,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClD,MAAM,YAAY,GAAG,SAAS,CAAC,WAAW,CAAC,IAAM,EAAE,CAAA;QAEnD,IAAI,YAAY,GAAG,YAAY,EAAE,CAAC;YAChC,OAAO,IAAA,2BAAa,EAAC,GAAG,EAAE,WAAW,EAAE,4BAA4B,WAAW,iBAAiB,EAAE;gBAC/F,YAAY,EAAE,WAAW;gBACzB,QAAQ,EAAE,QAAQ,CAAC,IAAI;aACxB,CAAC,CAAA;QACJ,CAAC;QAED,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;AACH,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
import { Request, Response, NextFunction } from 'express';
/**
* Blocks requests for companies with lapsed or unactivated subscriptions.
* Must be applied after `requireTenant`.
*
* Guarantees on success:
* req.company.status is not SUSPENDED or PENDING
*/
export declare function requireSubscription(req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>> | undefined;
//# sourceMappingURL=requireSubscription.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireSubscription.d.ts","sourceRoot":"","sources":["../../src/middleware/requireSubscription.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAKzD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,kDAgBlF"}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireSubscription = requireSubscription;
const authHelpers_1 = require("./authHelpers");
const BLOCKED_STATUSES = ['SUSPENDED', 'PENDING'];
/**
* Blocks requests for companies with lapsed or unactivated subscriptions.
* Must be applied after `requireTenant`.
*
* Guarantees on success:
* req.company.status is not SUSPENDED or PENDING
*/
function requireSubscription(req, res, next) {
const company = req.company;
if (!company)
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'No company context');
if (BLOCKED_STATUSES.includes(company.status)) {
return (0, authHelpers_1.sendPaymentRequired)(res, `subscription_${company.status.toLowerCase()}`, company.status === 'SUSPENDED'
? 'Your account has been suspended. Please contact support or renew your subscription.'
: 'Your account is pending activation. Please complete your subscription setup.', { billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/billing` });
}
next();
}
//# sourceMappingURL=requireSubscription.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireSubscription.js","sourceRoot":"","sources":["../../src/middleware/requireSubscription.ts"],"names":[],"mappings":";;AAYA,kDAgBC;AA3BD,+CAAqE;AAErE,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;AAEjD;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACjF,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAA;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,oBAAoB,CAAC,CAAA;IAEnF,IAAI,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9C,OAAO,IAAA,iCAAmB,EACxB,GAAG,EACH,gBAAgB,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,EAC9C,OAAO,CAAC,MAAM,KAAK,WAAW;YAC5B,CAAC,CAAC,qFAAqF;YACvF,CAAC,CAAC,8EAA8E,EAClF,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,UAAU,EAAE,CACnE,CAAA;IACH,CAAC;IAED,IAAI,EAAE,CAAA;AACR,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
import { Request, Response, NextFunction } from 'express';
/**
* Loads the company record and validates it exists.
* Must be applied after `requireCompanyAuth`.
*
* Guarantees on success:
* req.company — full Company record
* req.companyId — string id (already set by requireCompanyAuth)
*/
export declare function requireTenant(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
//# sourceMappingURL=requireTenant.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireTenant.d.ts","sourceRoot":"","sources":["../../src/middleware/requireTenant.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAIzD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,2DAYlF"}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireTenant = requireTenant;
const prisma_1 = require("../lib/prisma");
const authHelpers_1 = require("./authHelpers");
/**
* Loads the company record and validates it exists.
* Must be applied after `requireCompanyAuth`.
*
* Guarantees on success:
* req.company — full Company record
* req.companyId — string id (already set by requireCompanyAuth)
*/
async function requireTenant(req, res, next) {
if (!req.companyId) {
return (0, authHelpers_1.sendUnauthorized)(res, 'unauthenticated', 'Tenant context missing — requireCompanyAuth must run first');
}
const company = await prisma_1.prisma.company.findUnique({ where: { id: req.companyId } });
if (!company) {
return (0, authHelpers_1.sendUnauthorized)(res, 'company_not_found', 'Company not found');
}
req.company = company;
next();
}
//# sourceMappingURL=requireTenant.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"requireTenant.js","sourceRoot":"","sources":["../../src/middleware/requireTenant.ts"],"names":[],"mappings":";;AAYA,sCAYC;AAvBD,0CAAsC;AACtC,+CAAgD;AAEhD;;;;;;;GAOG;AACI,KAAK,UAAU,aAAa,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACjF,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;QACnB,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,iBAAiB,EAAE,4DAA4D,CAAC,CAAA;IAC/G,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,eAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACjF,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAA;IACxE,CAAC;IAED,GAAG,CAAC,OAAO,GAAG,OAAO,CAAA;IACrB,IAAI,EAAE,CAAA;AACR,CAAC"}
+69
View File
@@ -0,0 +1,69 @@
export declare function listBillingAccounts(query: {
q?: string;
status?: string;
plan?: string;
page: number;
pageSize: number;
}): Promise<{
data: any[];
total: any;
stats: {
billingAccountCount: any;
openInvoiceCount: any;
pastDueInvoiceCount: any;
paidInvoiceCount: any;
accountsReceivableTotal: any;
recognizedRevenueTotal: any;
};
}>;
export declare function getBillingAccountDetail(companyId: string): Promise<any>;
export declare function updateBillingAccount(billingAccountId: string, data: {
legalName?: string;
billingEmail?: string;
billingAddress?: any;
taxId?: string | null;
taxExempt?: boolean;
invoiceTerms?: string;
netTermsDays?: number;
}, adminId: string, ip?: string): Promise<any>;
export declare function setDunningPaused(billingAccountId: string, paused: boolean, adminId: string, ip?: string): Promise<any>;
export declare function createDraftInvoice(billingAccountId: string, data: {
subscriptionId?: string | null;
invoiceType: string;
currency?: string;
dueAt?: string | null;
isSubscriptionBlocking?: boolean;
adminReason?: string | null;
lineItems: Array<{
type: string;
description: string;
quantity: number;
unitAmount: number;
periodStart?: string | null;
periodEnd?: string | null;
}>;
}, adminId: string, ip?: string): Promise<any>;
export declare function finalizeInvoice(invoiceId: string, adminId: string, ip?: string): Promise<any>;
export declare function payInvoice(invoiceId: string, data: {
amount?: number;
paymentMethodId?: string | null;
providerPaymentId?: string | null;
}, adminId: string, ip?: string): Promise<any>;
export declare function retryInvoicePayment(invoiceId: string, data: {
paymentMethodId?: string | null;
}, adminId: string, ip?: string): Promise<any>;
export declare function voidInvoice(invoiceId: string, reason: string, adminId: string, ip?: string): Promise<any>;
export declare function markInvoiceUncollectible(invoiceId: string, reason: string, adminId: string, ip?: string): Promise<any>;
export declare function issueCreditNote(invoiceId: string, data: {
amount: number;
reason: string;
}, adminId: string, ip?: string): Promise<any>;
export declare function issueRefund(invoiceId: string, data: {
amount: number;
reason: string;
}, adminId: string, ip?: string): Promise<any>;
export declare function getInvoicePdf(invoiceId: string): Promise<{
pdfBuffer: Buffer<ArrayBufferLike>;
invoiceNumber: any;
}>;
//# sourceMappingURL=admin.billing.service.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"admin.billing.service.d.ts","sourceRoot":"","sources":["../../../src/modules/admin/admin.billing.service.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More