archetecture security fix
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
> @rentaldrivego/api@1.0.0 prebuild
|
||||
> npm run build --workspace @rentaldrivego/types
|
||||
|
||||
|
||||
> @rentaldrivego/types@1.0.0 build
|
||||
> tsc
|
||||
|
||||
⠙[1G[0K⠙[1G[0K
|
||||
> @rentaldrivego/api@1.0.0 build
|
||||
> tsc
|
||||
|
||||
⠙[1G[0K⠙[1G[0K
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export declare const corsOrigins: string[];
|
||||
export declare function createApp(): import("express-serve-static-core").Express;
|
||||
//# sourceMappingURL=app.d.ts.map
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+218
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"errorMiddleware.js","sourceRoot":"","sources":["../../../src/http/errors/errorMiddleware.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+22
@@ -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
@@ -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"}
|
||||
Vendored
+47
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||
Vendored
+15
@@ -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
@@ -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"}
|
||||
Vendored
+21
@@ -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
@@ -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"}
|
||||
Vendored
+78
@@ -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
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/upload/index.ts"],"names":[],"mappings":"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"}
|
||||
+6
@@ -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
@@ -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"}
|
||||
Vendored
+15
@@ -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
@@ -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"}
|
||||
Vendored
+4
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+21
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+5
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+259
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+68
@@ -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
|
||||
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
export declare function isDatabaseUnavailableError(error: unknown): boolean;
|
||||
//# sourceMappingURL=isDatabaseUnavailable.d.ts.map
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare const prisma: any;
|
||||
//# sourceMappingURL=prisma.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/lib/prisma.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,KAIf,CAAA"}
|
||||
Vendored
+13
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import Redis from 'ioredis';
|
||||
export declare const redis: Redis;
|
||||
//# sourceMappingURL=redis.d.ts.map
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+15
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+11
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+131
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/lib/storage.ts"],"names":[],"mappings":";;;;;AAcA,wCAIC;AAED,oDAEC;AAED,sDAEC;AAED,sDAEC;AAOD,gEAkBC;AAuCD,kFAEC;AAED,kCAoBC;AAED,sDAcC;AAED,kCAKC;AA7ID,4CAAmB;AACnB,gDAAuB;AACvB,oDAA2B;AAE3B,MAAM,gBAAgB,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAClE,MAAM,eAAe,GAAG,cAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAC1D,MAAM,aAAa,GAAG,cAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAA;AACzD,MAAM,oBAAoB,GAAG,cAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAA;AACnE,MAAM,oBAAoB,GAAG;IAC3B,cAAI,CAAC,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,SAAS,CAAC;CAC7C,CAAA;AAID,SAAgB,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAClC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC7C,CAAC,CAAC,oBAAoB,CAAA;AAC1B,CAAC;AAED,SAAgB,oBAAoB;IAClC,OAAO,cAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,QAAQ,CAAC,CAAA;AAC9C,CAAC;AAED,SAAgB,qBAAqB;IACnC,OAAO,cAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,SAAS,CAAC,CAAA;AAC/C,CAAC;AAED,SAAgB,qBAAqB;IACnC,OAAO,oBAAoB,CAAA;AAC7B,CAAC;AAED,SAAS,YAAY,CAAC,UAAkB,EAAE,UAAkB;IAC1D,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;IACtD,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAA;AACtF,CAAC;AAED,SAAgB,0BAA0B;IACxC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IAEpC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,0FAA0F,CAAC,CAAA;IAC7G,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QAC1C,MAAM,cAAc,GAAG,CAAC,gBAAgB,EAAE,eAAe,EAAE,aAAa,CAAC,CAAA;QACzE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAA;QAClF,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,iFAAiF,WAAW,qBAAqB,WAAW,GAAG,CAChI,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,WAAW,CAAA;AACpB,CAAC;AAED,SAAS,iBAAiB,CAAC,UAA6B;IACtD,0BAA0B,EAAE,CAAA;IAC5B,MAAM,IAAI,GAAG,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAAA;IACvF,YAAE,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACvC,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC7C,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC7D,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC7D,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9D,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9D,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC5D,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,uBAAuB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC5E,CAAC;AAED,SAAS,gBAAgB;IACvB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,aAAa;QACzB,OAAO,CAAC,GAAG,CAAC,yBAAyB;QACrC,iCAAiC,CAClC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACtB,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAgB;IAC9C,MAAM,MAAM,GAAG,WAAW,CAAA;IAC1B,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IACpC,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3B,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;AAC5C,CAAC;AAED,SAAgB,mCAAmC,CAAC,UAAkB;IACpE,OAAO,GAAG,gBAAgB,EAAE,qBAAqB,UAAU,gBAAgB,CAAA;AAC7E,CAAC;AAEM,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,MAAc,EACd,QAAiB,EACjB,aAAgC,eAAe,CAAC,MAAM,CAAC;IAEvD,MAAM,WAAW,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;IACjD,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;IACjD,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACrD,CAAC;IACD,YAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE7C,MAAM,QAAQ,GAAG,QAAQ;QACvB,CAAC,CAAC,GAAG,QAAQ,MAAM;QACnB,CAAC,CAAC,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAA;IAEnD,YAAE,CAAC,aAAa,CAAC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;IAEzD,OAAO,GAAG,UAAU,EAAE,YAAY,MAAM,IAAI,QAAQ,EAAE,CAAA;AACxD,CAAC;AAED,SAAgB,qBAAqB,CAAC,QAAgB;IACpD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,MAAM,KAAK,GAAG,CAAC,oBAAoB,EAAE,EAAE,qBAAqB,EAAE,EAAE,GAAG,qBAAqB,EAAE,EAAE,0BAA0B,EAAE,CAAC,CAAA;IACzH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC1C,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC;YAAE,SAAQ;QAC3C,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,OAAO,QAAQ,CAAA;QACjB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,QAAgB;IAChD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,QAAQ,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;IACzB,CAAC;AACH,CAAC"}
|
||||
+13
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"rateLimiter.js","sourceRoot":"","sources":["../../src/middleware/rateLimiter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yEAA8D;AAE9D,+CAAwD;AACxD,+DAAiE;AAGjE,MAAM,oBAAoB,GAAG;IAC3B,IAAA,qCAAoB,EAAC,OAAO,CAAC;IAC7B,IAAA,qCAAoB,EAAC,UAAU,CAAC;IAChC,IAAA,qCAAoB,EAAC,QAAQ,CAAC;CAC/B,CAAA;AAED,SAAS,UAAU,CAAC,YAAgC,EAAE,IAAY;IAChE,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;YAAE,OAAO,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,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,IAAI,IAAI,CAAA;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,GAAY;IACnC,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAA;IACvC,KAAK,MAAM,IAAI,IAAI,oBAAoB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;QAC5C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;IACzB,CAAC;IAED,OAAO,cAAc,CAAC,GAAG,CAAC,CAAA;AAC5B,CAAC;AAED,SAAS,wBAAwB,CAAC,GAAY;IAC5C,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IAEvB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAA,4BAAmB,EAAC,KAAK,CAAC,CAAA;QAC1C,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,MAAM,cAAc,GAAG,CAAC,GAAY,EAAE,EAAE,CAAC,IAAA,mCAAc,EAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;AAErE,oFAAoF;AACpF,8EAA8E;AAC9E,iCAAiC;AACpB,QAAA,WAAW,GAAG,IAAA,4BAAS,EAAC;IACnC,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,aAAa;IACvC,GAAG,EAAE,EAAE;IACP,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,sBAAsB,EAAE,IAAI;IAC5B,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC;IAC1C,OAAO,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,2CAA2C,EAAE,UAAU,EAAE,GAAG,EAAE;CAC/G,CAAC,CAAA;AAEF,2DAA2D;AAC9C,QAAA,UAAU,GAAG,IAAA,4BAAS,EAAC;IAClC,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,WAAW;IAChC,GAAG,EAAE,GAAG;IACR,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE;QACpB,MAAM,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAA;QAC9C,MAAM,SAAS,GAAI,GAAW,CAAC,SAAS,IAAI,EAAE,CAAA;QAC9C,MAAM,QAAQ,GAAI,GAAW,CAAC,QAAQ,IAAI,EAAE,CAAA;QAC5C,OAAO,GAAG,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,QAAQ,IAAI,WAAW,EAAE,CAAA;IACpE,CAAC;IACD,OAAO,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,GAAG,EAAE;CACzF,CAAC,CAAA;AAEF,8DAA8D;AACjD,QAAA,aAAa,GAAG,IAAA,4BAAS,EAAC;IACrC,QAAQ,EAAE,EAAE,GAAG,IAAI;IACnB,GAAG,EAAE,EAAE;IACP,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC;IAC1C,OAAO,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,GAAG,EAAE;CACzF,CAAC,CAAA;AAEF,oCAAoC;AACvB,QAAA,YAAY,GAAG,IAAA,4BAAS,EAAC;IACpC,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,GAAG,EAAE,GAAG;IACR,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE;QACpB,MAAM,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,CAAA;QAC9B,OAAO,GAAG,EAAE,IAAI,wBAAwB,CAAC,GAAG,CAAC,IAAI,WAAW,EAAE,CAAA;IAChE,CAAC;IACD,OAAO,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,yBAAyB,EAAE,UAAU,EAAE,GAAG,EAAE;CAC7F,CAAC,CAAA;AAGF,gFAAgF;AAChF,sEAAsE;AACzD,QAAA,YAAY,GAAG,IAAA,4BAAS,EAAC;IACpC,QAAQ,EAAE,EAAE,GAAG,IAAI;IACnB,GAAG,EAAE,GAAG;IACR,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE;QACpB,MAAM,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAA;QAC9C,MAAM,SAAS,GAAI,GAAW,CAAC,SAAS,IAAI,EAAE,CAAA;QAC9C,MAAM,UAAU,GAAI,GAAW,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAA;QAClD,MAAM,QAAQ,GAAI,GAAW,CAAC,QAAQ,IAAI,EAAE,CAAA;QAC5C,MAAM,OAAO,GAAI,GAAW,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,CAAA;QAC5C,OAAO,GAAG,EAAE,IAAI,SAAS,IAAI,QAAQ,IAAI,UAAU,IAAI,QAAQ,IAAI,OAAO,IAAI,WAAW,EAAE,CAAA;IAC7F,CAAC;IACD,OAAO,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,mCAAmC,EAAE,UAAU,EAAE,GAAG,EAAE;CACvG,CAAC,CAAA"}
|
||||
+3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"requireAdminAuth.js","sourceRoot":"","sources":["../../src/middleware/requireAdminAuth.ts"],"names":[],"mappings":"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"}
|
||||
+3
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,69 @@
|
||||
export declare function listBillingAccounts(query: {
|
||||
q?: string;
|
||||
status?: string;
|
||||
plan?: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<{
|
||||
data: any[];
|
||||
total: any;
|
||||
stats: {
|
||||
billingAccountCount: any;
|
||||
openInvoiceCount: any;
|
||||
pastDueInvoiceCount: any;
|
||||
paidInvoiceCount: any;
|
||||
accountsReceivableTotal: any;
|
||||
recognizedRevenueTotal: any;
|
||||
};
|
||||
}>;
|
||||
export declare function getBillingAccountDetail(companyId: string): Promise<any>;
|
||||
export declare function updateBillingAccount(billingAccountId: string, data: {
|
||||
legalName?: string;
|
||||
billingEmail?: string;
|
||||
billingAddress?: any;
|
||||
taxId?: string | null;
|
||||
taxExempt?: boolean;
|
||||
invoiceTerms?: string;
|
||||
netTermsDays?: number;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function setDunningPaused(billingAccountId: string, paused: boolean, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function createDraftInvoice(billingAccountId: string, data: {
|
||||
subscriptionId?: string | null;
|
||||
invoiceType: string;
|
||||
currency?: string;
|
||||
dueAt?: string | null;
|
||||
isSubscriptionBlocking?: boolean;
|
||||
adminReason?: string | null;
|
||||
lineItems: Array<{
|
||||
type: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitAmount: number;
|
||||
periodStart?: string | null;
|
||||
periodEnd?: string | null;
|
||||
}>;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function finalizeInvoice(invoiceId: string, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function payInvoice(invoiceId: string, data: {
|
||||
amount?: number;
|
||||
paymentMethodId?: string | null;
|
||||
providerPaymentId?: string | null;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function retryInvoicePayment(invoiceId: string, data: {
|
||||
paymentMethodId?: string | null;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function voidInvoice(invoiceId: string, reason: string, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function markInvoiceUncollectible(invoiceId: string, reason: string, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function issueCreditNote(invoiceId: string, data: {
|
||||
amount: number;
|
||||
reason: string;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function issueRefund(invoiceId: string, data: {
|
||||
amount: number;
|
||||
reason: string;
|
||||
}, adminId: string, ip?: string): Promise<any>;
|
||||
export declare function getInvoicePdf(invoiceId: string): Promise<{
|
||||
pdfBuffer: Buffer<ArrayBufferLike>;
|
||||
invoiceNumber: any;
|
||||
}>;
|
||||
//# sourceMappingURL=admin.billing.service.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"admin.billing.service.d.ts","sourceRoot":"","sources":["../../../src/modules/admin/admin.billing.service.ts"],"names":[],"mappings":"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"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user