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
+6
View File
@@ -0,0 +1,6 @@
DATABASE_URL=postgresql://user:replace-with-password@host:5432/db
REDIS_URL=redis://localhost:6379
JWT_SECRET=replace-with-64-byte-random-secret
JWT_EXPIRY=8h
NODE_ENV=test
FILE_STORAGE_ROOT=/tmp/rentaldrivego-test-storage
+13
View File
@@ -0,0 +1,13 @@
> @rentaldrivego/api@1.0.0 prebuild
> npm run build --workspace @rentaldrivego/types
> @rentaldrivego/types@1.0.0 build
> tsc
⠙
> @rentaldrivego/api@1.0.0 build
> tsc
⠙
+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":"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"}
+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":"AA8VA,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;GA2H9H;AAED,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,gBAyC9D;AAED,wBAAsB,oBAAoB,CACxC,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE;IACJ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,EACD,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBAuCZ;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBAkCZ;AAED,wBAAsB,kBAAkB,CACtC,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE;IACJ,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,KAAK,CAAC;QACf,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAA;QACnB,QAAQ,EAAE,MAAM,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC,CAAA;CACH,EACD,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBAmEZ;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,gBAwLpF;AAED,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IACJ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC,EACD,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBAiGZ;AAED,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACzC,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBA0EZ;AAED,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,gBAmDhG;AAED,wBAAsB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,gBAgD7G;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACxC,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBA4EZ;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACxC,OAAO,EAAE,MAAM,EACf,EAAE,CAAC,EAAE,MAAM,gBA6EZ;AAED,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM;;;GAiEpD"}

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