chore: untrack api/dist + tsbuildinfo build artifacts
api/dist/ (468 files, 11MB) and admin/tsconfig.tsbuildinfo were committed before being added to .gitignore — the rule had no effect on the existing tracked copies. Untrack them now so future Docker rebuilds stop showing spurious diffs. Files stay on disk; rebuild regenerates everything. Also add *.tsbuildinfo to .gitignore so future tsc incremental caches stay out of git.
This commit is contained in:
6
api/dist/config/database.d.ts
vendored
6
api/dist/config/database.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
declare const prisma: PrismaClient<{
|
||||
log: ("error" | "query" | "warn")[];
|
||||
}, "error" | "query" | "warn", import("@prisma/client/runtime/library").DefaultArgs>;
|
||||
export { prisma };
|
||||
//# sourceMappingURL=database.d.ts.map
|
||||
1
api/dist/config/database.d.ts.map
vendored
1
api/dist/config/database.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/config/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,QAAA,MAAM,MAAM;;oFAEV,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
||||
10
api/dist/config/database.js
vendored
10
api/dist/config/database.js
vendored
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prisma = void 0;
|
||||
const client_1 = require("@prisma/client");
|
||||
const env_1 = require("./env");
|
||||
const prisma = new client_1.PrismaClient({
|
||||
log: env_1.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
exports.prisma = prisma;
|
||||
//# sourceMappingURL=database.js.map
|
||||
1
api/dist/config/database.js.map
vendored
1
api/dist/config/database.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/config/database.ts"],"names":[],"mappings":";;;AAAA,2CAA8C;AAC9C,+BAA4B;AAE5B,MAAM,MAAM,GAAG,IAAI,qBAAY,CAAC;IAC9B,GAAG,EAAE,SAAG,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAC7E,CAAC,CAAC;AAEM,wBAAM"}
|
||||
582
api/dist/config/env.d.ts
vendored
582
api/dist/config/env.d.ts
vendored
@@ -1,582 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
declare const envSchema: z.ZodObject<{
|
||||
NODE_ENV: z.ZodDefault<z.ZodEnum<["development", "production", "test"]>>;
|
||||
PORT: z.ZodDefault<z.ZodNumber>;
|
||||
API_URL: z.ZodDefault<z.ZodString>;
|
||||
ADMIN_URL: z.ZodDefault<z.ZodString>;
|
||||
DOMAIN: z.ZodDefault<z.ZodString>;
|
||||
INSTANCE_LABEL: z.ZodDefault<z.ZodString>;
|
||||
BUNKER_OPS_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
BUNKER_OPS_REMOTE_WRITE_URL: z.ZodDefault<z.ZodString>;
|
||||
DATABASE_URL: z.ZodString;
|
||||
REDIS_URL: z.ZodDefault<z.ZodString>;
|
||||
JWT_ACCESS_SECRET: z.ZodString;
|
||||
JWT_REFRESH_SECRET: z.ZodString;
|
||||
JWT_INVITE_SECRET: z.ZodString;
|
||||
JWT_ACCESS_EXPIRY: z.ZodDefault<z.ZodString>;
|
||||
JWT_REFRESH_EXPIRY: z.ZodDefault<z.ZodString>;
|
||||
ENCRYPTION_KEY: z.ZodOptional<z.ZodString>;
|
||||
INITIAL_ADMIN_EMAIL: z.ZodDefault<z.ZodString>;
|
||||
INITIAL_ADMIN_PASSWORD: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
|
||||
SMTP_HOST: z.ZodDefault<z.ZodString>;
|
||||
SMTP_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
SMTP_USER: z.ZodDefault<z.ZodString>;
|
||||
SMTP_PASS: z.ZodDefault<z.ZodString>;
|
||||
SMTP_FROM: z.ZodDefault<z.ZodString>;
|
||||
SMTP_FROM_NAME: z.ZodDefault<z.ZodString>;
|
||||
EMAIL_TEST_MODE: z.ZodDefault<z.ZodString>;
|
||||
TEST_EMAIL_RECIPIENT: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_URL: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_SYNC_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_WEBHOOK_SECRET: z.ZodDefault<z.ZodString>;
|
||||
LISTMONK_PROXY_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
REPRESENT_API_URL: z.ZodDefault<z.ZodString>;
|
||||
CORS_ORIGINS: z.ZodDefault<z.ZodString>;
|
||||
RATE_LIMIT_WINDOW_MS: z.ZodDefault<z.ZodNumber>;
|
||||
RATE_LIMIT_MAX: z.ZodDefault<z.ZodNumber>;
|
||||
MAPBOX_API_KEY: z.ZodOptional<z.ZodString>;
|
||||
GEOCODING_RATE_LIMIT_MS: z.ZodDefault<z.ZodNumber>;
|
||||
GEOCODING_CACHE_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
GEOCODING_CACHE_TTL_HOURS: z.ZodDefault<z.ZodNumber>;
|
||||
GOOGLE_MAPS_API_KEY: z.ZodOptional<z.ZodString>;
|
||||
GOOGLE_MAPS_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
GEOCODING_PARALLEL_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
GEOCODING_BATCH_SIZE: z.ZodDefault<z.ZodNumber>;
|
||||
BULK_GEOCODE_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
BULK_GEOCODE_MAX_BATCH: z.ZodDefault<z.ZodNumber>;
|
||||
NOCODB_URL: z.ZodDefault<z.ZodString>;
|
||||
NOCODB_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
NOCODB_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
N8N_URL: z.ZodDefault<z.ZodString>;
|
||||
N8N_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
N8N_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GITEA_URL: z.ZodDefault<z.ZodString>;
|
||||
GITEA_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GITEA_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MAILHOG_URL: z.ZodDefault<z.ZodString>;
|
||||
MAILHOG_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MINI_QR_URL: z.ZodDefault<z.ZodString>;
|
||||
MINI_QR_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MINI_QR_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
EXCALIDRAW_URL: z.ZodDefault<z.ZodString>;
|
||||
EXCALIDRAW_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
EXCALIDRAW_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
HOMEPAGE_URL: z.ZodDefault<z.ZodString>;
|
||||
HOMEPAGE_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
VAULTWARDEN_URL: z.ZodDefault<z.ZodString>;
|
||||
VAULTWARDEN_ADMIN_TOKEN: z.ZodDefault<z.ZodString>;
|
||||
VAULTWARDEN_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
ROCKETCHAT_URL: z.ZodDefault<z.ZodString>;
|
||||
ROCKETCHAT_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||
ROCKETCHAT_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||
ROCKETCHAT_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
ENABLE_CHAT: z.ZodDefault<z.ZodString>;
|
||||
GANCIO_URL: z.ZodDefault<z.ZodString>;
|
||||
GANCIO_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GANCIO_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GANCIO_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||
GANCIO_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||
GANCIO_SYNC_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
ENABLE_MEET: z.ZodDefault<z.ZodString>;
|
||||
JITSI_APP_ID: z.ZodDefault<z.ZodString>;
|
||||
JITSI_APP_SECRET: z.ZodDefault<z.ZodString>;
|
||||
JITSI_URL: z.ZodDefault<z.ZodString>;
|
||||
JITSI_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
PANGOLIN_API_URL: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
|
||||
PANGOLIN_API_KEY: z.ZodDefault<z.ZodString>;
|
||||
PANGOLIN_ORG_ID: z.ZodDefault<z.ZodString>;
|
||||
PANGOLIN_SITE_ID: z.ZodDefault<z.ZodString>;
|
||||
PANGOLIN_ENDPOINT: z.ZodDefault<z.ZodString>;
|
||||
PANGOLIN_NEWT_ID: z.ZodDefault<z.ZodString>;
|
||||
PANGOLIN_NEWT_SECRET: z.ZodDefault<z.ZodString>;
|
||||
NAR_DATA_DIR: z.ZodDefault<z.ZodString>;
|
||||
OVERPASS_API_URL: z.ZodDefault<z.ZodString>;
|
||||
OVERPASS_MIN_DELAY_MS: z.ZodDefault<z.ZodNumber>;
|
||||
AREA_IMPORT_MAX_GRID_POINTS: z.ZodDefault<z.ZodNumber>;
|
||||
ENABLE_PAYMENTS: z.ZodDefault<z.ZodString>;
|
||||
ENABLE_MEDIA_FEATURES: z.ZodDefault<z.ZodString>;
|
||||
MEDIA_API_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MEDIA_API_PUBLIC_URL: z.ZodDefault<z.ZodString>;
|
||||
MEDIA_ROOT: z.ZodDefault<z.ZodString>;
|
||||
MEDIA_UPLOADS: z.ZodDefault<z.ZodString>;
|
||||
MAX_UPLOAD_SIZE_GB: z.ZodDefault<z.ZodNumber>;
|
||||
GITEA_REGISTRY: z.ZodDefault<z.ZodString>;
|
||||
GITEA_REGISTRY_USER: z.ZodDefault<z.ZodString>;
|
||||
GITEA_REGISTRY_PASS: z.ZodDefault<z.ZodString>;
|
||||
GITEA_COMMENTS_ENABLED: z.ZodDefault<z.ZodString>;
|
||||
GITEA_API_TOKEN: z.ZodDefault<z.ZodString>;
|
||||
GITEA_COMMENTS_REPO_OWNER: z.ZodDefault<z.ZodString>;
|
||||
GITEA_COMMENTS_REPO_NAME: z.ZodDefault<z.ZodString>;
|
||||
GITEA_OAUTH_CLIENT_ID: z.ZodDefault<z.ZodString>;
|
||||
GITEA_OAUTH_CLIENT_SECRET: z.ZodDefault<z.ZodString>;
|
||||
ENABLE_SMS: z.ZodDefault<z.ZodString>;
|
||||
TERMUX_API_URL: z.ZodDefault<z.ZodString>;
|
||||
TERMUX_API_KEY: z.ZodDefault<z.ZodString>;
|
||||
SMS_DELAY_BETWEEN_MS: z.ZodDefault<z.ZodNumber>;
|
||||
SMS_MAX_RETRIES: z.ZodDefault<z.ZodNumber>;
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
||||
CODE_SERVER_URL: z.ZodDefault<z.ZodString>;
|
||||
CODE_SERVER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MKDOCS_PREVIEW_URL: z.ZodDefault<z.ZodString>;
|
||||
MKDOCS_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
MKDOCS_DOCS_PATH: z.ZodDefault<z.ZodString>;
|
||||
MKDOCS_CONFIG_PATH: z.ZodDefault<z.ZodString>;
|
||||
MKDOCS_CONTAINER_NAME: z.ZodDefault<z.ZodString>;
|
||||
MKDOCS_SITE_SERVER_URL: z.ZodDefault<z.ZodString>;
|
||||
MKDOCS_SITE_SERVER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
PROMETHEUS_URL: z.ZodDefault<z.ZodString>;
|
||||
PROMETHEUS_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GRAFANA_URL: z.ZodDefault<z.ZodString>;
|
||||
GRAFANA_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GRAFANA_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
ALERTMANAGER_URL: z.ZodDefault<z.ZodString>;
|
||||
ALERTMANAGER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
ALERTMANAGER_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
CADVISOR_URL: z.ZodDefault<z.ZodString>;
|
||||
CADVISOR_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
NODE_EXPORTER_URL: z.ZodDefault<z.ZodString>;
|
||||
NODE_EXPORTER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
REDIS_EXPORTER_URL: z.ZodDefault<z.ZodString>;
|
||||
REDIS_EXPORTER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
GOTIFY_URL: z.ZodDefault<z.ZodString>;
|
||||
GOTIFY_PORT: z.ZodDefault<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
NODE_ENV: "development" | "production" | "test";
|
||||
PORT: number;
|
||||
API_URL: string;
|
||||
ADMIN_URL: string;
|
||||
DOMAIN: string;
|
||||
INSTANCE_LABEL: string;
|
||||
BUNKER_OPS_ENABLED: string;
|
||||
BUNKER_OPS_REMOTE_WRITE_URL: string;
|
||||
DATABASE_URL: string;
|
||||
REDIS_URL: string;
|
||||
JWT_ACCESS_SECRET: string;
|
||||
JWT_REFRESH_SECRET: string;
|
||||
JWT_INVITE_SECRET: string;
|
||||
JWT_ACCESS_EXPIRY: string;
|
||||
JWT_REFRESH_EXPIRY: string;
|
||||
INITIAL_ADMIN_EMAIL: string;
|
||||
INITIAL_ADMIN_PASSWORD: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: number;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_FROM: string;
|
||||
SMTP_FROM_NAME: string;
|
||||
EMAIL_TEST_MODE: string;
|
||||
TEST_EMAIL_RECIPIENT: string;
|
||||
LISTMONK_URL: string;
|
||||
LISTMONK_ADMIN_USER: string;
|
||||
LISTMONK_ADMIN_PASSWORD: string;
|
||||
LISTMONK_SYNC_ENABLED: string;
|
||||
LISTMONK_WEBHOOK_SECRET: string;
|
||||
LISTMONK_PROXY_PORT: number;
|
||||
REPRESENT_API_URL: string;
|
||||
CORS_ORIGINS: string;
|
||||
RATE_LIMIT_WINDOW_MS: number;
|
||||
RATE_LIMIT_MAX: number;
|
||||
GEOCODING_RATE_LIMIT_MS: number;
|
||||
GEOCODING_CACHE_ENABLED: string;
|
||||
GEOCODING_CACHE_TTL_HOURS: number;
|
||||
GOOGLE_MAPS_ENABLED: string;
|
||||
GEOCODING_PARALLEL_ENABLED: string;
|
||||
GEOCODING_BATCH_SIZE: number;
|
||||
BULK_GEOCODE_ENABLED: string;
|
||||
BULK_GEOCODE_MAX_BATCH: number;
|
||||
NOCODB_URL: string;
|
||||
NOCODB_PORT: number;
|
||||
NOCODB_EMBED_PORT: number;
|
||||
N8N_URL: string;
|
||||
N8N_PORT: number;
|
||||
N8N_EMBED_PORT: number;
|
||||
GITEA_URL: string;
|
||||
GITEA_PORT: number;
|
||||
GITEA_EMBED_PORT: number;
|
||||
MAILHOG_URL: string;
|
||||
MAILHOG_EMBED_PORT: number;
|
||||
MINI_QR_URL: string;
|
||||
MINI_QR_PORT: number;
|
||||
MINI_QR_EMBED_PORT: number;
|
||||
EXCALIDRAW_URL: string;
|
||||
EXCALIDRAW_PORT: number;
|
||||
EXCALIDRAW_EMBED_PORT: number;
|
||||
HOMEPAGE_URL: string;
|
||||
HOMEPAGE_EMBED_PORT: number;
|
||||
VAULTWARDEN_URL: string;
|
||||
VAULTWARDEN_ADMIN_TOKEN: string;
|
||||
VAULTWARDEN_EMBED_PORT: number;
|
||||
ROCKETCHAT_URL: string;
|
||||
ROCKETCHAT_ADMIN_USER: string;
|
||||
ROCKETCHAT_ADMIN_PASSWORD: string;
|
||||
ROCKETCHAT_EMBED_PORT: number;
|
||||
ENABLE_CHAT: string;
|
||||
GANCIO_URL: string;
|
||||
GANCIO_PORT: number;
|
||||
GANCIO_EMBED_PORT: number;
|
||||
GANCIO_ADMIN_USER: string;
|
||||
GANCIO_ADMIN_PASSWORD: string;
|
||||
GANCIO_SYNC_ENABLED: string;
|
||||
ENABLE_MEET: string;
|
||||
JITSI_APP_ID: string;
|
||||
JITSI_APP_SECRET: string;
|
||||
JITSI_URL: string;
|
||||
JITSI_EMBED_PORT: number;
|
||||
PANGOLIN_API_URL: string;
|
||||
PANGOLIN_API_KEY: string;
|
||||
PANGOLIN_ORG_ID: string;
|
||||
PANGOLIN_SITE_ID: string;
|
||||
PANGOLIN_ENDPOINT: string;
|
||||
PANGOLIN_NEWT_ID: string;
|
||||
PANGOLIN_NEWT_SECRET: string;
|
||||
NAR_DATA_DIR: string;
|
||||
OVERPASS_API_URL: string;
|
||||
OVERPASS_MIN_DELAY_MS: number;
|
||||
AREA_IMPORT_MAX_GRID_POINTS: number;
|
||||
ENABLE_PAYMENTS: string;
|
||||
ENABLE_MEDIA_FEATURES: string;
|
||||
MEDIA_API_PORT: number;
|
||||
MEDIA_API_PUBLIC_URL: string;
|
||||
MEDIA_ROOT: string;
|
||||
MEDIA_UPLOADS: string;
|
||||
MAX_UPLOAD_SIZE_GB: number;
|
||||
GITEA_REGISTRY: string;
|
||||
GITEA_REGISTRY_USER: string;
|
||||
GITEA_REGISTRY_PASS: string;
|
||||
GITEA_COMMENTS_ENABLED: string;
|
||||
GITEA_API_TOKEN: string;
|
||||
GITEA_COMMENTS_REPO_OWNER: string;
|
||||
GITEA_COMMENTS_REPO_NAME: string;
|
||||
GITEA_OAUTH_CLIENT_ID: string;
|
||||
GITEA_OAUTH_CLIENT_SECRET: string;
|
||||
ENABLE_SMS: string;
|
||||
TERMUX_API_URL: string;
|
||||
TERMUX_API_KEY: string;
|
||||
SMS_DELAY_BETWEEN_MS: number;
|
||||
SMS_MAX_RETRIES: number;
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS: number;
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS: number;
|
||||
CODE_SERVER_URL: string;
|
||||
CODE_SERVER_PORT: number;
|
||||
MKDOCS_PREVIEW_URL: string;
|
||||
MKDOCS_PORT: number;
|
||||
MKDOCS_DOCS_PATH: string;
|
||||
MKDOCS_CONFIG_PATH: string;
|
||||
MKDOCS_CONTAINER_NAME: string;
|
||||
MKDOCS_SITE_SERVER_URL: string;
|
||||
MKDOCS_SITE_SERVER_PORT: number;
|
||||
PROMETHEUS_URL: string;
|
||||
PROMETHEUS_PORT: number;
|
||||
GRAFANA_URL: string;
|
||||
GRAFANA_PORT: number;
|
||||
GRAFANA_EMBED_PORT: number;
|
||||
ALERTMANAGER_URL: string;
|
||||
ALERTMANAGER_PORT: number;
|
||||
ALERTMANAGER_EMBED_PORT: number;
|
||||
CADVISOR_URL: string;
|
||||
CADVISOR_PORT: number;
|
||||
NODE_EXPORTER_URL: string;
|
||||
NODE_EXPORTER_PORT: number;
|
||||
REDIS_EXPORTER_URL: string;
|
||||
REDIS_EXPORTER_PORT: number;
|
||||
GOTIFY_URL: string;
|
||||
GOTIFY_PORT: number;
|
||||
ENCRYPTION_KEY?: string | undefined;
|
||||
MAPBOX_API_KEY?: string | undefined;
|
||||
GOOGLE_MAPS_API_KEY?: string | undefined;
|
||||
}, {
|
||||
DATABASE_URL: string;
|
||||
JWT_ACCESS_SECRET: string;
|
||||
JWT_REFRESH_SECRET: string;
|
||||
JWT_INVITE_SECRET: string;
|
||||
NODE_ENV?: "development" | "production" | "test" | undefined;
|
||||
PORT?: number | undefined;
|
||||
API_URL?: string | undefined;
|
||||
ADMIN_URL?: string | undefined;
|
||||
DOMAIN?: string | undefined;
|
||||
INSTANCE_LABEL?: string | undefined;
|
||||
BUNKER_OPS_ENABLED?: string | undefined;
|
||||
BUNKER_OPS_REMOTE_WRITE_URL?: string | undefined;
|
||||
REDIS_URL?: string | undefined;
|
||||
JWT_ACCESS_EXPIRY?: string | undefined;
|
||||
JWT_REFRESH_EXPIRY?: string | undefined;
|
||||
ENCRYPTION_KEY?: string | undefined;
|
||||
INITIAL_ADMIN_EMAIL?: string | undefined;
|
||||
INITIAL_ADMIN_PASSWORD?: string | undefined;
|
||||
SMTP_HOST?: string | undefined;
|
||||
SMTP_PORT?: number | undefined;
|
||||
SMTP_USER?: string | undefined;
|
||||
SMTP_PASS?: string | undefined;
|
||||
SMTP_FROM?: string | undefined;
|
||||
SMTP_FROM_NAME?: string | undefined;
|
||||
EMAIL_TEST_MODE?: string | undefined;
|
||||
TEST_EMAIL_RECIPIENT?: string | undefined;
|
||||
LISTMONK_URL?: string | undefined;
|
||||
LISTMONK_ADMIN_USER?: string | undefined;
|
||||
LISTMONK_ADMIN_PASSWORD?: string | undefined;
|
||||
LISTMONK_SYNC_ENABLED?: string | undefined;
|
||||
LISTMONK_WEBHOOK_SECRET?: string | undefined;
|
||||
LISTMONK_PROXY_PORT?: number | undefined;
|
||||
REPRESENT_API_URL?: string | undefined;
|
||||
CORS_ORIGINS?: string | undefined;
|
||||
RATE_LIMIT_WINDOW_MS?: number | undefined;
|
||||
RATE_LIMIT_MAX?: number | undefined;
|
||||
MAPBOX_API_KEY?: string | undefined;
|
||||
GEOCODING_RATE_LIMIT_MS?: number | undefined;
|
||||
GEOCODING_CACHE_ENABLED?: string | undefined;
|
||||
GEOCODING_CACHE_TTL_HOURS?: number | undefined;
|
||||
GOOGLE_MAPS_API_KEY?: string | undefined;
|
||||
GOOGLE_MAPS_ENABLED?: string | undefined;
|
||||
GEOCODING_PARALLEL_ENABLED?: string | undefined;
|
||||
GEOCODING_BATCH_SIZE?: number | undefined;
|
||||
BULK_GEOCODE_ENABLED?: string | undefined;
|
||||
BULK_GEOCODE_MAX_BATCH?: number | undefined;
|
||||
NOCODB_URL?: string | undefined;
|
||||
NOCODB_PORT?: number | undefined;
|
||||
NOCODB_EMBED_PORT?: number | undefined;
|
||||
N8N_URL?: string | undefined;
|
||||
N8N_PORT?: number | undefined;
|
||||
N8N_EMBED_PORT?: number | undefined;
|
||||
GITEA_URL?: string | undefined;
|
||||
GITEA_PORT?: number | undefined;
|
||||
GITEA_EMBED_PORT?: number | undefined;
|
||||
MAILHOG_URL?: string | undefined;
|
||||
MAILHOG_EMBED_PORT?: number | undefined;
|
||||
MINI_QR_URL?: string | undefined;
|
||||
MINI_QR_PORT?: number | undefined;
|
||||
MINI_QR_EMBED_PORT?: number | undefined;
|
||||
EXCALIDRAW_URL?: string | undefined;
|
||||
EXCALIDRAW_PORT?: number | undefined;
|
||||
EXCALIDRAW_EMBED_PORT?: number | undefined;
|
||||
HOMEPAGE_URL?: string | undefined;
|
||||
HOMEPAGE_EMBED_PORT?: number | undefined;
|
||||
VAULTWARDEN_URL?: string | undefined;
|
||||
VAULTWARDEN_ADMIN_TOKEN?: string | undefined;
|
||||
VAULTWARDEN_EMBED_PORT?: number | undefined;
|
||||
ROCKETCHAT_URL?: string | undefined;
|
||||
ROCKETCHAT_ADMIN_USER?: string | undefined;
|
||||
ROCKETCHAT_ADMIN_PASSWORD?: string | undefined;
|
||||
ROCKETCHAT_EMBED_PORT?: number | undefined;
|
||||
ENABLE_CHAT?: string | undefined;
|
||||
GANCIO_URL?: string | undefined;
|
||||
GANCIO_PORT?: number | undefined;
|
||||
GANCIO_EMBED_PORT?: number | undefined;
|
||||
GANCIO_ADMIN_USER?: string | undefined;
|
||||
GANCIO_ADMIN_PASSWORD?: string | undefined;
|
||||
GANCIO_SYNC_ENABLED?: string | undefined;
|
||||
ENABLE_MEET?: string | undefined;
|
||||
JITSI_APP_ID?: string | undefined;
|
||||
JITSI_APP_SECRET?: string | undefined;
|
||||
JITSI_URL?: string | undefined;
|
||||
JITSI_EMBED_PORT?: number | undefined;
|
||||
PANGOLIN_API_URL?: string | undefined;
|
||||
PANGOLIN_API_KEY?: string | undefined;
|
||||
PANGOLIN_ORG_ID?: string | undefined;
|
||||
PANGOLIN_SITE_ID?: string | undefined;
|
||||
PANGOLIN_ENDPOINT?: string | undefined;
|
||||
PANGOLIN_NEWT_ID?: string | undefined;
|
||||
PANGOLIN_NEWT_SECRET?: string | undefined;
|
||||
NAR_DATA_DIR?: string | undefined;
|
||||
OVERPASS_API_URL?: string | undefined;
|
||||
OVERPASS_MIN_DELAY_MS?: number | undefined;
|
||||
AREA_IMPORT_MAX_GRID_POINTS?: number | undefined;
|
||||
ENABLE_PAYMENTS?: string | undefined;
|
||||
ENABLE_MEDIA_FEATURES?: string | undefined;
|
||||
MEDIA_API_PORT?: number | undefined;
|
||||
MEDIA_API_PUBLIC_URL?: string | undefined;
|
||||
MEDIA_ROOT?: string | undefined;
|
||||
MEDIA_UPLOADS?: string | undefined;
|
||||
MAX_UPLOAD_SIZE_GB?: number | undefined;
|
||||
GITEA_REGISTRY?: string | undefined;
|
||||
GITEA_REGISTRY_USER?: string | undefined;
|
||||
GITEA_REGISTRY_PASS?: string | undefined;
|
||||
GITEA_COMMENTS_ENABLED?: string | undefined;
|
||||
GITEA_API_TOKEN?: string | undefined;
|
||||
GITEA_COMMENTS_REPO_OWNER?: string | undefined;
|
||||
GITEA_COMMENTS_REPO_NAME?: string | undefined;
|
||||
GITEA_OAUTH_CLIENT_ID?: string | undefined;
|
||||
GITEA_OAUTH_CLIENT_SECRET?: string | undefined;
|
||||
ENABLE_SMS?: string | undefined;
|
||||
TERMUX_API_URL?: string | undefined;
|
||||
TERMUX_API_KEY?: string | undefined;
|
||||
SMS_DELAY_BETWEEN_MS?: number | undefined;
|
||||
SMS_MAX_RETRIES?: number | undefined;
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS?: number | undefined;
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS?: number | undefined;
|
||||
CODE_SERVER_URL?: string | undefined;
|
||||
CODE_SERVER_PORT?: number | undefined;
|
||||
MKDOCS_PREVIEW_URL?: string | undefined;
|
||||
MKDOCS_PORT?: number | undefined;
|
||||
MKDOCS_DOCS_PATH?: string | undefined;
|
||||
MKDOCS_CONFIG_PATH?: string | undefined;
|
||||
MKDOCS_CONTAINER_NAME?: string | undefined;
|
||||
MKDOCS_SITE_SERVER_URL?: string | undefined;
|
||||
MKDOCS_SITE_SERVER_PORT?: number | undefined;
|
||||
PROMETHEUS_URL?: string | undefined;
|
||||
PROMETHEUS_PORT?: number | undefined;
|
||||
GRAFANA_URL?: string | undefined;
|
||||
GRAFANA_PORT?: number | undefined;
|
||||
GRAFANA_EMBED_PORT?: number | undefined;
|
||||
ALERTMANAGER_URL?: string | undefined;
|
||||
ALERTMANAGER_PORT?: number | undefined;
|
||||
ALERTMANAGER_EMBED_PORT?: number | undefined;
|
||||
CADVISOR_URL?: string | undefined;
|
||||
CADVISOR_PORT?: number | undefined;
|
||||
NODE_EXPORTER_URL?: string | undefined;
|
||||
NODE_EXPORTER_PORT?: number | undefined;
|
||||
REDIS_EXPORTER_URL?: string | undefined;
|
||||
REDIS_EXPORTER_PORT?: number | undefined;
|
||||
GOTIFY_URL?: string | undefined;
|
||||
GOTIFY_PORT?: number | undefined;
|
||||
}>;
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
export declare const env: {
|
||||
NODE_ENV: "development" | "production" | "test";
|
||||
PORT: number;
|
||||
API_URL: string;
|
||||
ADMIN_URL: string;
|
||||
DOMAIN: string;
|
||||
INSTANCE_LABEL: string;
|
||||
BUNKER_OPS_ENABLED: string;
|
||||
BUNKER_OPS_REMOTE_WRITE_URL: string;
|
||||
DATABASE_URL: string;
|
||||
REDIS_URL: string;
|
||||
JWT_ACCESS_SECRET: string;
|
||||
JWT_REFRESH_SECRET: string;
|
||||
JWT_INVITE_SECRET: string;
|
||||
JWT_ACCESS_EXPIRY: string;
|
||||
JWT_REFRESH_EXPIRY: string;
|
||||
INITIAL_ADMIN_EMAIL: string;
|
||||
INITIAL_ADMIN_PASSWORD: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: number;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_FROM: string;
|
||||
SMTP_FROM_NAME: string;
|
||||
EMAIL_TEST_MODE: string;
|
||||
TEST_EMAIL_RECIPIENT: string;
|
||||
LISTMONK_URL: string;
|
||||
LISTMONK_ADMIN_USER: string;
|
||||
LISTMONK_ADMIN_PASSWORD: string;
|
||||
LISTMONK_SYNC_ENABLED: string;
|
||||
LISTMONK_WEBHOOK_SECRET: string;
|
||||
LISTMONK_PROXY_PORT: number;
|
||||
REPRESENT_API_URL: string;
|
||||
CORS_ORIGINS: string;
|
||||
RATE_LIMIT_WINDOW_MS: number;
|
||||
RATE_LIMIT_MAX: number;
|
||||
GEOCODING_RATE_LIMIT_MS: number;
|
||||
GEOCODING_CACHE_ENABLED: string;
|
||||
GEOCODING_CACHE_TTL_HOURS: number;
|
||||
GOOGLE_MAPS_ENABLED: string;
|
||||
GEOCODING_PARALLEL_ENABLED: string;
|
||||
GEOCODING_BATCH_SIZE: number;
|
||||
BULK_GEOCODE_ENABLED: string;
|
||||
BULK_GEOCODE_MAX_BATCH: number;
|
||||
NOCODB_URL: string;
|
||||
NOCODB_PORT: number;
|
||||
NOCODB_EMBED_PORT: number;
|
||||
N8N_URL: string;
|
||||
N8N_PORT: number;
|
||||
N8N_EMBED_PORT: number;
|
||||
GITEA_URL: string;
|
||||
GITEA_PORT: number;
|
||||
GITEA_EMBED_PORT: number;
|
||||
MAILHOG_URL: string;
|
||||
MAILHOG_EMBED_PORT: number;
|
||||
MINI_QR_URL: string;
|
||||
MINI_QR_PORT: number;
|
||||
MINI_QR_EMBED_PORT: number;
|
||||
EXCALIDRAW_URL: string;
|
||||
EXCALIDRAW_PORT: number;
|
||||
EXCALIDRAW_EMBED_PORT: number;
|
||||
HOMEPAGE_URL: string;
|
||||
HOMEPAGE_EMBED_PORT: number;
|
||||
VAULTWARDEN_URL: string;
|
||||
VAULTWARDEN_ADMIN_TOKEN: string;
|
||||
VAULTWARDEN_EMBED_PORT: number;
|
||||
ROCKETCHAT_URL: string;
|
||||
ROCKETCHAT_ADMIN_USER: string;
|
||||
ROCKETCHAT_ADMIN_PASSWORD: string;
|
||||
ROCKETCHAT_EMBED_PORT: number;
|
||||
ENABLE_CHAT: string;
|
||||
GANCIO_URL: string;
|
||||
GANCIO_PORT: number;
|
||||
GANCIO_EMBED_PORT: number;
|
||||
GANCIO_ADMIN_USER: string;
|
||||
GANCIO_ADMIN_PASSWORD: string;
|
||||
GANCIO_SYNC_ENABLED: string;
|
||||
ENABLE_MEET: string;
|
||||
JITSI_APP_ID: string;
|
||||
JITSI_APP_SECRET: string;
|
||||
JITSI_URL: string;
|
||||
JITSI_EMBED_PORT: number;
|
||||
PANGOLIN_API_URL: string;
|
||||
PANGOLIN_API_KEY: string;
|
||||
PANGOLIN_ORG_ID: string;
|
||||
PANGOLIN_SITE_ID: string;
|
||||
PANGOLIN_ENDPOINT: string;
|
||||
PANGOLIN_NEWT_ID: string;
|
||||
PANGOLIN_NEWT_SECRET: string;
|
||||
NAR_DATA_DIR: string;
|
||||
OVERPASS_API_URL: string;
|
||||
OVERPASS_MIN_DELAY_MS: number;
|
||||
AREA_IMPORT_MAX_GRID_POINTS: number;
|
||||
ENABLE_PAYMENTS: string;
|
||||
ENABLE_MEDIA_FEATURES: string;
|
||||
MEDIA_API_PORT: number;
|
||||
MEDIA_API_PUBLIC_URL: string;
|
||||
MEDIA_ROOT: string;
|
||||
MEDIA_UPLOADS: string;
|
||||
MAX_UPLOAD_SIZE_GB: number;
|
||||
GITEA_REGISTRY: string;
|
||||
GITEA_REGISTRY_USER: string;
|
||||
GITEA_REGISTRY_PASS: string;
|
||||
GITEA_COMMENTS_ENABLED: string;
|
||||
GITEA_API_TOKEN: string;
|
||||
GITEA_COMMENTS_REPO_OWNER: string;
|
||||
GITEA_COMMENTS_REPO_NAME: string;
|
||||
GITEA_OAUTH_CLIENT_ID: string;
|
||||
GITEA_OAUTH_CLIENT_SECRET: string;
|
||||
ENABLE_SMS: string;
|
||||
TERMUX_API_URL: string;
|
||||
TERMUX_API_KEY: string;
|
||||
SMS_DELAY_BETWEEN_MS: number;
|
||||
SMS_MAX_RETRIES: number;
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS: number;
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS: number;
|
||||
CODE_SERVER_URL: string;
|
||||
CODE_SERVER_PORT: number;
|
||||
MKDOCS_PREVIEW_URL: string;
|
||||
MKDOCS_PORT: number;
|
||||
MKDOCS_DOCS_PATH: string;
|
||||
MKDOCS_CONFIG_PATH: string;
|
||||
MKDOCS_CONTAINER_NAME: string;
|
||||
MKDOCS_SITE_SERVER_URL: string;
|
||||
MKDOCS_SITE_SERVER_PORT: number;
|
||||
PROMETHEUS_URL: string;
|
||||
PROMETHEUS_PORT: number;
|
||||
GRAFANA_URL: string;
|
||||
GRAFANA_PORT: number;
|
||||
GRAFANA_EMBED_PORT: number;
|
||||
ALERTMANAGER_URL: string;
|
||||
ALERTMANAGER_PORT: number;
|
||||
ALERTMANAGER_EMBED_PORT: number;
|
||||
CADVISOR_URL: string;
|
||||
CADVISOR_PORT: number;
|
||||
NODE_EXPORTER_URL: string;
|
||||
NODE_EXPORTER_PORT: number;
|
||||
REDIS_EXPORTER_URL: string;
|
||||
REDIS_EXPORTER_PORT: number;
|
||||
GOTIFY_URL: string;
|
||||
GOTIFY_PORT: number;
|
||||
ENCRYPTION_KEY?: string | undefined;
|
||||
MAPBOX_API_KEY?: string | undefined;
|
||||
GOOGLE_MAPS_API_KEY?: string | undefined;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=env.d.ts.map
|
||||
1
api/dist/config/env.d.ts.map
vendored
1
api/dist/config/env.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2Nb,CAAC;AAEH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAY5C,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
|
||||
202
api/dist/config/env.js
vendored
202
api/dist/config/env.js
vendored
@@ -1,202 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.env = void 0;
|
||||
const dotenv_1 = __importDefault(require("dotenv"));
|
||||
const zod_1 = require("zod");
|
||||
dotenv_1.default.config();
|
||||
const envSchema = zod_1.z.object({
|
||||
// Server
|
||||
NODE_ENV: zod_1.z.enum(['development', 'production', 'test']).default('development'),
|
||||
PORT: zod_1.z.coerce.number().default(4000),
|
||||
API_URL: zod_1.z.string().default('http://localhost:4000'),
|
||||
ADMIN_URL: zod_1.z.string().default('http://localhost:3000'),
|
||||
DOMAIN: zod_1.z.string().default('cmlite.org'),
|
||||
// Bunker Ops (Fleet Management)
|
||||
INSTANCE_LABEL: zod_1.z.string().default(''),
|
||||
BUNKER_OPS_ENABLED: zod_1.z.string().default('false'),
|
||||
BUNKER_OPS_REMOTE_WRITE_URL: zod_1.z.string().default(''),
|
||||
// Database
|
||||
DATABASE_URL: zod_1.z.string(),
|
||||
// Redis
|
||||
REDIS_URL: zod_1.z.string().default('redis://redis-changemaker:6379'),
|
||||
// JWT
|
||||
JWT_ACCESS_SECRET: zod_1.z.string().min(32),
|
||||
JWT_REFRESH_SECRET: zod_1.z.string().min(32),
|
||||
JWT_INVITE_SECRET: zod_1.z.string().min(32),
|
||||
JWT_ACCESS_EXPIRY: zod_1.z.string().default('15m'),
|
||||
JWT_REFRESH_EXPIRY: zod_1.z.string().default('7d'),
|
||||
// Encryption (for DB-stored secrets like SMTP password; falls back to JWT_ACCESS_SECRET)
|
||||
ENCRYPTION_KEY: zod_1.z.string().min(32, 'ENCRYPTION_KEY must be at least 32 characters').optional(),
|
||||
// Initial Super Admin (auto-created during database seeding)
|
||||
INITIAL_ADMIN_EMAIL: zod_1.z.string().email().default('admin@cmlite.org'),
|
||||
INITIAL_ADMIN_PASSWORD: zod_1.z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS')
|
||||
.refine((val) => val !== 'REQUIRED_STRONG_PASSWORD_CHANGE_THIS', { message: 'INITIAL_ADMIN_PASSWORD must be changed from the default placeholder value' }),
|
||||
// SMTP
|
||||
SMTP_HOST: zod_1.z.string().default('mailhog-changemaker'),
|
||||
SMTP_PORT: zod_1.z.coerce.number().default(1025),
|
||||
SMTP_USER: zod_1.z.string().default(''),
|
||||
SMTP_PASS: zod_1.z.string().default(''),
|
||||
SMTP_FROM: zod_1.z.string().default('noreply@cmlite.org'),
|
||||
SMTP_FROM_NAME: zod_1.z.string().default('Changemaker Lite'),
|
||||
EMAIL_TEST_MODE: zod_1.z.string().default('true'),
|
||||
TEST_EMAIL_RECIPIENT: zod_1.z.string().default('admin@cmlite.org'),
|
||||
// Listmonk
|
||||
LISTMONK_URL: zod_1.z.string().default('http://listmonk-app:9000'),
|
||||
LISTMONK_ADMIN_USER: zod_1.z.string().default('admin'),
|
||||
LISTMONK_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||
LISTMONK_SYNC_ENABLED: zod_1.z.string().default('false'),
|
||||
LISTMONK_WEBHOOK_SECRET: zod_1.z.string().default(''),
|
||||
LISTMONK_PROXY_PORT: zod_1.z.coerce.number().default(9002),
|
||||
// Represent API (Canadian electoral data)
|
||||
REPRESENT_API_URL: zod_1.z.string().default('https://represent.opennorth.ca'),
|
||||
// CORS
|
||||
CORS_ORIGINS: zod_1.z.string().default('http://localhost:3000'),
|
||||
// Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS: zod_1.z.coerce.number().default(15 * 60 * 1000),
|
||||
RATE_LIMIT_MAX: zod_1.z.coerce.number().default(500),
|
||||
// Geocoding
|
||||
MAPBOX_API_KEY: zod_1.z.string().optional(),
|
||||
GEOCODING_RATE_LIMIT_MS: zod_1.z.coerce.number().default(1100),
|
||||
GEOCODING_CACHE_ENABLED: zod_1.z.string().default('true'),
|
||||
GEOCODING_CACHE_TTL_HOURS: zod_1.z.coerce.number().default(24),
|
||||
// Phase 2: Performance & Accuracy
|
||||
GOOGLE_MAPS_API_KEY: zod_1.z.string().optional(),
|
||||
GOOGLE_MAPS_ENABLED: zod_1.z.string().default('false'),
|
||||
GEOCODING_PARALLEL_ENABLED: zod_1.z.string().default('true'),
|
||||
GEOCODING_BATCH_SIZE: zod_1.z.coerce.number().default(10),
|
||||
// Bulk Re-Geocoding (Phase 3)
|
||||
BULK_GEOCODE_ENABLED: zod_1.z.string().default('true'),
|
||||
BULK_GEOCODE_MAX_BATCH: zod_1.z.coerce.number().default(5000),
|
||||
// Platform Services (NocoDB, n8n, Gitea)
|
||||
NOCODB_URL: zod_1.z.string().default('http://changemaker-v2-nocodb:8080'),
|
||||
NOCODB_PORT: zod_1.z.coerce.number().default(8091),
|
||||
NOCODB_EMBED_PORT: zod_1.z.coerce.number().default(8881),
|
||||
N8N_URL: zod_1.z.string().default('http://n8n-changemaker:5678'),
|
||||
N8N_PORT: zod_1.z.coerce.number().default(5678),
|
||||
N8N_EMBED_PORT: zod_1.z.coerce.number().default(8882),
|
||||
GITEA_URL: zod_1.z.string().default('http://gitea-changemaker:3000'),
|
||||
GITEA_PORT: zod_1.z.coerce.number().default(3030),
|
||||
GITEA_EMBED_PORT: zod_1.z.coerce.number().default(8883),
|
||||
// MailHog (email testing UI)
|
||||
MAILHOG_URL: zod_1.z.string().default('http://mailhog-changemaker:8025'),
|
||||
MAILHOG_EMBED_PORT: zod_1.z.coerce.number().default(8884),
|
||||
// Mini QR (QR code generator)
|
||||
MINI_QR_URL: zod_1.z.string().default('http://mini-qr:8080'),
|
||||
MINI_QR_PORT: zod_1.z.coerce.number().default(8089),
|
||||
MINI_QR_EMBED_PORT: zod_1.z.coerce.number().default(8885),
|
||||
// Excalidraw (collaborative whiteboard)
|
||||
EXCALIDRAW_URL: zod_1.z.string().default('http://excalidraw-changemaker:80'),
|
||||
EXCALIDRAW_PORT: zod_1.z.coerce.number().default(8090),
|
||||
EXCALIDRAW_EMBED_PORT: zod_1.z.coerce.number().default(8886),
|
||||
// Homepage (service dashboard)
|
||||
HOMEPAGE_URL: zod_1.z.string().default('http://homepage-changemaker:3000'),
|
||||
HOMEPAGE_EMBED_PORT: zod_1.z.coerce.number().default(8887),
|
||||
// Vaultwarden (password manager)
|
||||
VAULTWARDEN_URL: zod_1.z.string().default('http://vaultwarden-changemaker:80'),
|
||||
VAULTWARDEN_ADMIN_TOKEN: zod_1.z.string().default(''),
|
||||
VAULTWARDEN_EMBED_PORT: zod_1.z.coerce.number().default(8890),
|
||||
// Rocket.Chat (team chat)
|
||||
ROCKETCHAT_URL: zod_1.z.string().default('http://rocketchat-changemaker:3000'),
|
||||
ROCKETCHAT_ADMIN_USER: zod_1.z.string().default(''),
|
||||
ROCKETCHAT_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||
ROCKETCHAT_EMBED_PORT: zod_1.z.coerce.number().default(8891),
|
||||
ENABLE_CHAT: zod_1.z.string().default('false'),
|
||||
// Gancio (event management)
|
||||
GANCIO_URL: zod_1.z.string().default('http://gancio-changemaker:13120'),
|
||||
GANCIO_PORT: zod_1.z.coerce.number().default(8092),
|
||||
GANCIO_EMBED_PORT: zod_1.z.coerce.number().default(8892),
|
||||
GANCIO_ADMIN_USER: zod_1.z.string().default('admin'),
|
||||
GANCIO_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||
GANCIO_SYNC_ENABLED: zod_1.z.string().default('false'),
|
||||
// Jitsi Meet (video conferencing)
|
||||
ENABLE_MEET: zod_1.z.string().default('false'),
|
||||
JITSI_APP_ID: zod_1.z.string().default('changemaker'),
|
||||
JITSI_APP_SECRET: zod_1.z.string().default(''),
|
||||
JITSI_URL: zod_1.z.string().default('http://jitsi-web-changemaker:80'),
|
||||
JITSI_EMBED_PORT: zod_1.z.coerce.number().default(8893),
|
||||
// Pangolin (tunnel / reverse proxy)
|
||||
PANGOLIN_API_URL: zod_1.z.string()
|
||||
.default('')
|
||||
.refine((url) => !url || url.startsWith('https://'), { message: 'PANGOLIN_API_URL must use HTTPS for secure credential transmission' }),
|
||||
PANGOLIN_API_KEY: zod_1.z.string().default(''),
|
||||
PANGOLIN_ORG_ID: zod_1.z.string().default(''),
|
||||
PANGOLIN_SITE_ID: zod_1.z.string().default(''),
|
||||
PANGOLIN_ENDPOINT: zod_1.z.string().default(''),
|
||||
PANGOLIN_NEWT_ID: zod_1.z.string().default(''),
|
||||
PANGOLIN_NEWT_SECRET: zod_1.z.string().default(''),
|
||||
// NAR (National Address Register)
|
||||
NAR_DATA_DIR: zod_1.z.string().default('/data'),
|
||||
// Overpass / Area Import
|
||||
OVERPASS_API_URL: zod_1.z.string().default('https://overpass-api.de/api/interpreter'),
|
||||
OVERPASS_MIN_DELAY_MS: zod_1.z.coerce.number().default(30000),
|
||||
AREA_IMPORT_MAX_GRID_POINTS: zod_1.z.coerce.number().default(500),
|
||||
// Payments (Stripe)
|
||||
ENABLE_PAYMENTS: zod_1.z.string().default('false'),
|
||||
// Media Management
|
||||
ENABLE_MEDIA_FEATURES: zod_1.z.string().default('false'),
|
||||
MEDIA_API_PORT: zod_1.z.coerce.number().default(4100),
|
||||
MEDIA_API_PUBLIC_URL: zod_1.z.string().default('http://media-api:4100'),
|
||||
MEDIA_ROOT: zod_1.z.string().default('/media/library'),
|
||||
MEDIA_UPLOADS: zod_1.z.string().default('/media/uploads'),
|
||||
MAX_UPLOAD_SIZE_GB: zod_1.z.coerce.number().default(10),
|
||||
// Container Registry
|
||||
GITEA_REGISTRY: zod_1.z.string().default('gitea.bnkops.com/admin'),
|
||||
GITEA_REGISTRY_USER: zod_1.z.string().default(''),
|
||||
GITEA_REGISTRY_PASS: zod_1.z.string().default(''),
|
||||
// Gitea Docs Comments
|
||||
GITEA_COMMENTS_ENABLED: zod_1.z.string().default('false'),
|
||||
GITEA_API_TOKEN: zod_1.z.string().default(''),
|
||||
GITEA_COMMENTS_REPO_OWNER: zod_1.z.string().default(''),
|
||||
GITEA_COMMENTS_REPO_NAME: zod_1.z.string().default('docs-comments'),
|
||||
GITEA_OAUTH_CLIENT_ID: zod_1.z.string().default(''),
|
||||
GITEA_OAUTH_CLIENT_SECRET: zod_1.z.string().default(''),
|
||||
// SMS Campaigns (Termux Android bridge)
|
||||
ENABLE_SMS: zod_1.z.string().default('false'),
|
||||
TERMUX_API_URL: zod_1.z.string().default('http://10.0.0.193:5001'),
|
||||
TERMUX_API_KEY: zod_1.z.string().default(''),
|
||||
SMS_DELAY_BETWEEN_MS: zod_1.z.coerce.number().default(3000),
|
||||
SMS_MAX_RETRIES: zod_1.z.coerce.number().default(3),
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS: zod_1.z.coerce.number().default(30000),
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS: zod_1.z.coerce.number().default(30000),
|
||||
// Docs / Code Server
|
||||
CODE_SERVER_URL: zod_1.z.string().default('http://code-server-changemaker:8080'),
|
||||
CODE_SERVER_PORT: zod_1.z.coerce.number().default(8888),
|
||||
MKDOCS_PREVIEW_URL: zod_1.z.string().default('http://mkdocs-changemaker:8000'),
|
||||
MKDOCS_PORT: zod_1.z.coerce.number().default(4003),
|
||||
MKDOCS_DOCS_PATH: zod_1.z.string().default('/mkdocs/docs'),
|
||||
MKDOCS_CONFIG_PATH: zod_1.z.string().default('/mkdocs/mkdocs.yml'),
|
||||
MKDOCS_CONTAINER_NAME: zod_1.z.string().default('mkdocs-changemaker'),
|
||||
MKDOCS_SITE_SERVER_URL: zod_1.z.string().default('http://mkdocs-site-server-changemaker:80'),
|
||||
MKDOCS_SITE_SERVER_PORT: zod_1.z.coerce.number().default(4004),
|
||||
// Monitoring Services (behind 'monitoring' profile)
|
||||
PROMETHEUS_URL: zod_1.z.string().default('http://prometheus-changemaker:9090'),
|
||||
PROMETHEUS_PORT: zod_1.z.coerce.number().default(9090),
|
||||
GRAFANA_URL: zod_1.z.string().default('http://grafana-changemaker:3000'),
|
||||
GRAFANA_PORT: zod_1.z.coerce.number().default(3005),
|
||||
GRAFANA_EMBED_PORT: zod_1.z.coerce.number().default(8894),
|
||||
ALERTMANAGER_URL: zod_1.z.string().default('http://alertmanager-changemaker:9093'),
|
||||
ALERTMANAGER_PORT: zod_1.z.coerce.number().default(9093),
|
||||
ALERTMANAGER_EMBED_PORT: zod_1.z.coerce.number().default(8895),
|
||||
CADVISOR_URL: zod_1.z.string().default('http://cadvisor-changemaker:8080'),
|
||||
CADVISOR_PORT: zod_1.z.coerce.number().default(8086),
|
||||
NODE_EXPORTER_URL: zod_1.z.string().default('http://node-exporter-changemaker:9100'),
|
||||
NODE_EXPORTER_PORT: zod_1.z.coerce.number().default(9100),
|
||||
REDIS_EXPORTER_URL: zod_1.z.string().default('http://redis-exporter-changemaker:9121'),
|
||||
REDIS_EXPORTER_PORT: zod_1.z.coerce.number().default(9121),
|
||||
GOTIFY_URL: zod_1.z.string().default('http://gotify-changemaker:80'),
|
||||
GOTIFY_PORT: zod_1.z.coerce.number().default(8889),
|
||||
});
|
||||
function validateEnv() {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
console.error('Invalid environment variables:');
|
||||
console.error(result.error.flatten().fieldErrors);
|
||||
process.exit(1);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
exports.env = validateEnv();
|
||||
//# sourceMappingURL=env.js.map
|
||||
1
api/dist/config/env.js.map
vendored
1
api/dist/config/env.js.map
vendored
File diff suppressed because one or more lines are too long
3
api/dist/config/redis.d.ts
vendored
3
api/dist/config/redis.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
export declare const redis: Redis;
|
||||
//# sourceMappingURL=redis.d.ts.map
|
||||
1
api/dist/config/redis.d.ts.map
vendored
1
api/dist/config/redis.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/config/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAI5B,eAAO,MAAM,KAAK,OAOhB,CAAC"}
|
||||
24
api/dist/config/redis.js
vendored
24
api/dist/config/redis.js
vendored
@@ -1,24 +0,0 @@
|
||||
"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"));
|
||||
const env_1 = require("./env");
|
||||
const logger_1 = require("../utils/logger");
|
||||
exports.redis = new ioredis_1.default(env_1.env.REDIS_URL, {
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: true,
|
||||
retryStrategy(times) {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
});
|
||||
exports.redis.on('connect', () => {
|
||||
logger_1.logger.info('Redis connected');
|
||||
});
|
||||
exports.redis.on('error', (err) => {
|
||||
logger_1.logger.error('Redis connection error:', err);
|
||||
});
|
||||
//# sourceMappingURL=redis.js.map
|
||||
1
api/dist/config/redis.js.map
vendored
1
api/dist/config/redis.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/config/redis.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA4B;AAC5B,+BAA4B;AAC5B,4CAAyC;AAE5B,QAAA,KAAK,GAAG,IAAI,iBAAK,CAAC,SAAG,CAAC,SAAS,EAAE;IAC5C,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,CAAC,KAAK;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAC,CAAC;AAEH,aAAK,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,eAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEH,aAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;IACxB,eAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC"}
|
||||
2
api/dist/media-server.d.ts
vendored
2
api/dist/media-server.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=media-server.d.ts.map
|
||||
1
api/dist/media-server.d.ts.map
vendored
1
api/dist/media-server.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"media-server.d.ts","sourceRoot":"","sources":["../src/media-server.ts"],"names":[],"mappings":""}
|
||||
172
api/dist/media-server.js
vendored
172
api/dist/media-server.js
vendored
@@ -1,172 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fastify_1 = __importDefault(require("fastify"));
|
||||
const cors_1 = __importDefault(require("@fastify/cors"));
|
||||
const multipart_1 = __importDefault(require("@fastify/multipart"));
|
||||
const env_1 = require("./config/env");
|
||||
const logger_1 = require("./utils/logger");
|
||||
const videos_routes_1 = require("./modules/media/routes/videos.routes");
|
||||
const video_streaming_routes_1 = require("./modules/media/routes/video-streaming.routes");
|
||||
const reactions_routes_1 = require("./modules/media/routes/reactions.routes");
|
||||
const public_routes_1 = require("./modules/media/routes/public.routes");
|
||||
const chat_stream_routes_1 = require("./modules/media/routes/chat-stream.routes");
|
||||
const comments_routes_1 = require("./modules/media/routes/comments.routes");
|
||||
const upload_routes_1 = require("./modules/media/routes/upload.routes");
|
||||
const video_actions_routes_1 = require("./modules/media/routes/video-actions.routes");
|
||||
const video_schedule_routes_1 = require("./modules/media/routes/video-schedule.routes");
|
||||
const video_tracking_routes_1 = require("./modules/media/routes/video-tracking.routes");
|
||||
const comment_admin_routes_1 = require("./modules/media/routes/comment-admin.routes");
|
||||
const chat_notifications_routes_1 = require("./modules/media/routes/chat-notifications.routes");
|
||||
const chat_threads_routes_1 = require("./modules/media/routes/chat-threads.routes");
|
||||
const user_profile_routes_1 = require("./modules/media/routes/user-profile.routes");
|
||||
const shorts_routes_1 = require("./modules/media/routes/shorts.routes");
|
||||
const upvote_routes_1 = require("./modules/media/routes/upvote.routes");
|
||||
const video_schedule_queue_service_1 = require("./services/video-schedule-queue.service");
|
||||
const video_fetch_queue_service_1 = require("./services/video-fetch-queue.service");
|
||||
const fetch_routes_1 = require("./modules/media/routes/fetch.routes");
|
||||
const playlists_public_routes_1 = require("./modules/media/routes/playlists-public.routes");
|
||||
const playlists_user_routes_1 = require("./modules/media/routes/playlists-user.routes");
|
||||
const playlists_admin_routes_1 = require("./modules/media/routes/playlists-admin.routes");
|
||||
const photos_routes_1 = require("./modules/media/routes/photos.routes");
|
||||
const photo_upload_routes_1 = require("./modules/media/routes/photo-upload.routes");
|
||||
const photo_albums_routes_1 = require("./modules/media/routes/photo-albums.routes");
|
||||
const photos_public_routes_1 = require("./modules/media/routes/photos-public.routes");
|
||||
const photo_engagement_routes_1 = require("./modules/media/routes/photo-engagement.routes");
|
||||
// Add BigInt serialization support for Prisma BigInt fields
|
||||
// This converts BigInt values to strings when JSON.stringify() is called
|
||||
BigInt.prototype.toJSON = function () {
|
||||
return this.toString();
|
||||
};
|
||||
const fastify = (0, fastify_1.default)({
|
||||
logger: {
|
||||
level: env_1.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
},
|
||||
maxParamLength: 500,
|
||||
trustProxy: true,
|
||||
});
|
||||
// Graceful shutdown handler
|
||||
process.on('SIGTERM', async () => {
|
||||
logger_1.logger.info('SIGTERM received, shutting down gracefully...');
|
||||
await video_schedule_queue_service_1.videoScheduleQueueService.close();
|
||||
await video_fetch_queue_service_1.videoFetchQueueService.close();
|
||||
fastify.close(() => {
|
||||
logger_1.logger.info('Media API server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
// Global error handlers
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger_1.logger.error('Unhandled Promise Rejection in Media API', { reason: JSON.stringify(reason), promise: JSON.stringify(promise) });
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger_1.logger.error('Uncaught Exception in Media API', { error: error instanceof Error ? error.message : JSON.stringify(error) });
|
||||
fastify.close(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
// Start server
|
||||
const start = async () => {
|
||||
try {
|
||||
// CORS configuration — allow admin app + MkDocs docs site
|
||||
const allowedOrigins = env_1.env.CORS_ORIGINS.split(',').map(o => o.trim());
|
||||
// Auto-add MkDocs origins so video cards/players work in docs
|
||||
const mkdocsOrigin = `http://localhost:${env_1.env.MKDOCS_PORT || 4003}`;
|
||||
if (!allowedOrigins.includes(mkdocsOrigin)) {
|
||||
allowedOrigins.push(mkdocsOrigin);
|
||||
}
|
||||
// Also allow the docs subdomain in production (docs.domain.org)
|
||||
for (const origin of [...allowedOrigins]) {
|
||||
const match = origin.match(/^(https?:\/\/)app\./);
|
||||
if (match) {
|
||||
const docsOrigin = origin.replace(/^(https?:\/\/)app\./, '$1docs.');
|
||||
if (!allowedOrigins.includes(docsOrigin)) {
|
||||
allowedOrigins.push(docsOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
await fastify.register(cors_1.default, {
|
||||
origin: (origin, cb) => {
|
||||
// Allow requests with no origin (mobile apps, curl, etc.)
|
||||
if (!origin) {
|
||||
cb(null, true);
|
||||
return;
|
||||
}
|
||||
// Check if origin is in allowed list
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
cb(null, true);
|
||||
}
|
||||
else {
|
||||
cb(new Error('CORS not allowed'), false);
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
});
|
||||
// Multipart support for file uploads (10GB limit)
|
||||
await fastify.register(multipart_1.default, {
|
||||
limits: {
|
||||
fileSize: env_1.env.MAX_UPLOAD_SIZE_GB * 1024 * 1024 * 1024,
|
||||
},
|
||||
});
|
||||
// Health check
|
||||
fastify.get('/health', async () => {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'media-api'
|
||||
};
|
||||
});
|
||||
// Register routes
|
||||
await fastify.register(videos_routes_1.videosRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(video_streaming_routes_1.videoStreamingRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(upload_routes_1.uploadRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(video_actions_routes_1.videoActionsRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(video_schedule_routes_1.videoScheduleRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(video_tracking_routes_1.videoTrackingRoutes, { prefix: '/api/track' });
|
||||
await fastify.register(reactions_routes_1.reactionsRoutes, { prefix: '/api/reactions' });
|
||||
await fastify.register(public_routes_1.publicRoutes, { prefix: '/api' });
|
||||
await fastify.register(comments_routes_1.commentsRoutes, { prefix: '/api' });
|
||||
await fastify.register(chat_stream_routes_1.chatStreamRoutes, { prefix: '/api' });
|
||||
await fastify.register(comment_admin_routes_1.commentAdminRoutes, { prefix: '/api/media' });
|
||||
await fastify.register(chat_notifications_routes_1.chatNotificationsRoutes, { prefix: '/api/media' });
|
||||
await fastify.register(chat_threads_routes_1.chatThreadsRoutes, { prefix: '/api/media' });
|
||||
await fastify.register(user_profile_routes_1.userProfileRoutes, { prefix: '/api/media' });
|
||||
await fastify.register(fetch_routes_1.fetchRoutes, { prefix: '/api/videos' });
|
||||
await fastify.register(shorts_routes_1.shortsRoutes, { prefix: '/api' });
|
||||
await fastify.register(upvote_routes_1.upvoteRoutes, { prefix: '/api' });
|
||||
await fastify.register(playlists_public_routes_1.playlistsPublicRoutes, { prefix: '/api/playlists' });
|
||||
await fastify.register(playlists_user_routes_1.playlistsUserRoutes, { prefix: '/api/playlists' });
|
||||
await fastify.register(playlists_admin_routes_1.playlistsAdminRoutes, { prefix: '/api/media' });
|
||||
// Photo gallery routes
|
||||
await fastify.register(photos_routes_1.photosRoutes, { prefix: '/api/photos' });
|
||||
await fastify.register(photo_upload_routes_1.photoUploadRoutes, { prefix: '/api/photos' });
|
||||
await fastify.register(photo_albums_routes_1.photoAlbumsRoutes, { prefix: '/api/albums' });
|
||||
await fastify.register(photos_public_routes_1.photosPublicRoutes, { prefix: '/api' });
|
||||
await fastify.register(photo_engagement_routes_1.photoEngagementRoutes, { prefix: '/api' });
|
||||
// 404 handler for unmatched routes
|
||||
fastify.setNotFoundHandler((_request, reply) => {
|
||||
reply.status(404).send({ error: { message: 'Route not found', code: 'NOT_FOUND' } });
|
||||
});
|
||||
const port = env_1.env.MEDIA_API_PORT;
|
||||
const host = '0.0.0.0';
|
||||
await fastify.listen({ port, host });
|
||||
logger_1.logger.info(`Media API listening on http://${host}:${port}`);
|
||||
// Start video schedule queue worker
|
||||
video_schedule_queue_service_1.videoScheduleQueueService.startWorker();
|
||||
logger_1.logger.info('Video schedule queue worker initialized');
|
||||
// Start video fetch queue worker
|
||||
video_fetch_queue_service_1.videoFetchQueueService.startWorker();
|
||||
logger_1.logger.info('Video fetch queue worker initialized');
|
||||
if (env_1.env.ENABLE_MEDIA_FEATURES !== 'true') {
|
||||
logger_1.logger.warn('Media features are disabled (ENABLE_MEDIA_FEATURES=false)');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Media API startup error', { error: err instanceof Error ? err.message : JSON.stringify(err) });
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
start();
|
||||
//# sourceMappingURL=media-server.js.map
|
||||
1
api/dist/media-server.js.map
vendored
1
api/dist/media-server.js.map
vendored
File diff suppressed because one or more lines are too long
4
api/dist/middleware/auth.middleware.d.ts
vendored
4
api/dist/middleware/auth.middleware.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
export declare function authenticate(req: Request, _res: Response, next: NextFunction): void;
|
||||
export declare function optionalAuth(req: Request, _res: Response, next: NextFunction): void;
|
||||
//# sourceMappingURL=auth.middleware.d.ts.map
|
||||
1
api/dist/middleware/auth.middleware.d.ts.map
vendored
1
api/dist/middleware/auth.middleware.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAa1D,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAoB5E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAsB5E"}
|
||||
52
api/dist/middleware/auth.middleware.js
vendored
52
api/dist/middleware/auth.middleware.js
vendored
@@ -1,52 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.authenticate = authenticate;
|
||||
exports.optionalAuth = optionalAuth;
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const env_1 = require("../config/env");
|
||||
const error_handler_1 = require("./error-handler");
|
||||
function authenticate(req, _res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header?.startsWith('Bearer ')) {
|
||||
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
||||
}
|
||||
const token = header.slice(7);
|
||||
try {
|
||||
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] });
|
||||
req.user = {
|
||||
id: payload.id,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
roles: payload.roles || [payload.role], // Backwards compat: old JWTs without roles
|
||||
};
|
||||
next();
|
||||
}
|
||||
catch {
|
||||
throw new error_handler_1.AppError(401, 'Invalid or expired token', 'INVALID_TOKEN');
|
||||
}
|
||||
}
|
||||
function optionalAuth(req, _res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header?.startsWith('Bearer ')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const token = header.slice(7);
|
||||
try {
|
||||
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] });
|
||||
req.user = {
|
||||
id: payload.id,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
roles: payload.roles || [payload.role],
|
||||
};
|
||||
}
|
||||
catch {
|
||||
// Token invalid — continue without user
|
||||
}
|
||||
next();
|
||||
}
|
||||
//# sourceMappingURL=auth.middleware.js.map
|
||||
1
api/dist/middleware/auth.middleware.js.map
vendored
1
api/dist/middleware/auth.middleware.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.middleware.js","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":";;;;;AAaA,oCAoBC;AAED,oCAsBC;AAxDD,gEAA+B;AAE/B,uCAAoC;AACpC,mDAA2C;AAS3C,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAiB,CAAC;QACpG,GAAG,CAAC,IAAI,GAAG;YACT,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,2CAA2C;SACpF,CAAC;QACF,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,eAAe,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAiB,CAAC;QACpG,GAAG,CAAC,IAAI,GAAG;YACT,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;SACvC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,wCAAwC;IAC1C,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
||||
8
api/dist/middleware/error-handler.d.ts
vendored
8
api/dist/middleware/error-handler.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
export declare class AppError extends Error {
|
||||
statusCode: number;
|
||||
code?: string | undefined;
|
||||
constructor(statusCode: number, message: string, code?: string | undefined);
|
||||
}
|
||||
export declare function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void;
|
||||
//# sourceMappingURL=error-handler.d.ts.map
|
||||
1
api/dist/middleware/error-handler.d.ts.map
vendored
1
api/dist/middleware/error-handler.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,qBAAa,QAAS,SAAQ,KAAK;IAExB,UAAU,EAAE,MAAM;IAElB,IAAI,CAAC,EAAE,MAAM;gBAFb,UAAU,EAAE,MAAM,EACzB,OAAO,EAAE,MAAM,EACR,IAAI,CAAC,EAAE,MAAM,YAAA;CAKvB;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,KAAK,EACV,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,YAAY,QAqCpB"}
|
||||
52
api/dist/middleware/error-handler.js
vendored
52
api/dist/middleware/error-handler.js
vendored
@@ -1,52 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppError = void 0;
|
||||
exports.errorHandler = errorHandler;
|
||||
const zod_1 = require("zod");
|
||||
const logger_1 = require("../utils/logger");
|
||||
const env_1 = require("../config/env");
|
||||
class AppError extends Error {
|
||||
statusCode;
|
||||
code;
|
||||
constructor(statusCode, message, code) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
exports.AppError = AppError;
|
||||
function errorHandler(err, _req, res, _next) {
|
||||
if (err instanceof AppError) {
|
||||
res.status(err.statusCode).json({
|
||||
error: {
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (err instanceof zod_1.ZodError) {
|
||||
const fieldErrors = err.flatten().fieldErrors;
|
||||
const errorCount = Object.keys(fieldErrors).length;
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation error',
|
||||
code: 'VALIDATION_ERROR',
|
||||
// Only expose details in development
|
||||
...(env_1.env.NODE_ENV === 'development' && { details: fieldErrors }),
|
||||
// In production, only show count
|
||||
...(env_1.env.NODE_ENV === 'production' && { fieldCount: errorCount }),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: env_1.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
|
||||
code: 'INTERNAL_ERROR',
|
||||
},
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=error-handler.js.map
|
||||
1
api/dist/middleware/error-handler.js.map
vendored
1
api/dist/middleware/error-handler.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":";;;AAgBA,oCAyCC;AAxDD,6BAA+B;AAC/B,4CAAyC;AACzC,uCAAoC;AAEpC,MAAa,QAAS,SAAQ,KAAK;IAExB;IAEA;IAHT,YACS,UAAkB,EACzB,OAAe,EACR,IAAa;QAEpB,KAAK,CAAC,OAAO,CAAC,CAAC;QAJR,eAAU,GAAV,UAAU,CAAQ;QAElB,SAAI,GAAJ,IAAI,CAAS;QAGpB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AATD,4BASC;AAED,SAAgB,YAAY,CAC1B,GAAU,EACV,IAAa,EACb,GAAa,EACb,KAAmB;IAEnB,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YAC9B,KAAK,EAAE;gBACL,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,IAAI,EAAE,GAAG,CAAC,IAAI;aACf;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,GAAG,YAAY,cAAQ,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC;QAC9C,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;QAEnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,OAAO,EAAE,kBAAkB;gBAC3B,IAAI,EAAE,kBAAkB;gBACxB,qCAAqC;gBACrC,GAAG,CAAC,SAAG,CAAC,QAAQ,KAAK,aAAa,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;gBAC/D,iCAAiC;gBACjC,GAAG,CAAC,SAAG,CAAC,QAAQ,KAAK,YAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;aACjE;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,eAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IAEtC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE;YACL,OAAO,EAAE,SAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO;YAC9E,IAAI,EAAE,gBAAgB;SACvB;KACF,CAAC,CAAC;AACL,CAAC"}
|
||||
24
api/dist/middleware/rate-limit.d.ts
vendored
24
api/dist/middleware/rate-limit.d.ts
vendored
@@ -1,24 +0,0 @@
|
||||
export declare const globalRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const emailRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const responseRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const shiftSignupRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const canvassVisitRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const canvassBulkVisitRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const gpsTrackingRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const canvassGeocodeRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const adTrackingRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const quickJoinRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const authRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const observabilityRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const docsAnalyticsRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const docsCommentAnonRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const docsCommentAuthRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const docsCommentFetchRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const profileViewRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const profileEditRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const profilePhotoRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const profilePasswordRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const eventSubmissionRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const errorReportRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
export declare const healthMetricsRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||
//# sourceMappingURL=rate-limit.d.ts.map
|
||||
1
api/dist/middleware/rate-limit.d.ts.map
vendored
1
api/dist/middleware/rate-limit.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,eAAe,sDAe1B,CAAC;AAEH,eAAO,MAAM,cAAc,sDAezB,CAAC;AAEH,eAAO,MAAM,iBAAiB,sDAe5B,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,sDAehC,CAAC;AAEH,eAAO,MAAM,yBAAyB,sDAepC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,sDAelC,CAAC;AAEH,eAAO,MAAM,mBAAmB,sDAe9B,CAAC;AAEH,eAAO,MAAM,kBAAkB,sDAe7B,CAAC;AAEH,eAAO,MAAM,aAAa,sDAexB,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,yBAAyB,sDAepC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,sDAehC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC"}
|
||||
379
api/dist/middleware/rate-limit.js
vendored
379
api/dist/middleware/rate-limit.js
vendored
@@ -1,379 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.healthMetricsRateLimit = exports.errorReportRateLimit = exports.eventSubmissionRateLimit = exports.profilePasswordRateLimit = exports.profilePhotoRateLimit = exports.profileEditRateLimit = exports.profileViewRateLimit = exports.docsCommentFetchRateLimit = exports.docsCommentAuthRateLimit = exports.docsCommentAnonRateLimit = exports.docsAnalyticsRateLimit = exports.observabilityRateLimit = exports.authRateLimit = exports.quickJoinRateLimit = exports.adTrackingRateLimit = exports.canvassGeocodeRateLimit = exports.gpsTrackingRateLimit = exports.canvassBulkVisitRateLimit = exports.canvassVisitRateLimit = exports.shiftSignupRateLimit = exports.responseRateLimit = exports.emailRateLimit = exports.globalRateLimit = void 0;
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
||||
const redis_1 = require("../config/redis");
|
||||
const env_1 = require("../config/env");
|
||||
exports.globalRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: env_1.env.RATE_LIMIT_WINDOW_MS,
|
||||
max: env_1.env.RATE_LIMIT_MAX,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:global:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many requests, please try again later',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.emailRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 30,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:email:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many email requests, please try again later',
|
||||
code: 'EMAIL_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.responseRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:response:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many response submissions, please try again later',
|
||||
code: 'RESPONSE_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.shiftSignupRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:shift-signup:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many signup attempts, please try again later',
|
||||
code: 'SHIFT_SIGNUP_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.canvassVisitRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 30,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:canvass-visit:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many visit recordings, please slow down',
|
||||
code: 'CANVASS_VISIT_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.canvassBulkVisitRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 5, // Stricter limit for bulk operations
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:canvass-visit-bulk:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many bulk visit recordings, please slow down',
|
||||
code: 'CANVASS_BULK_VISIT_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.gpsTrackingRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 6,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:gps-tracking:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many GPS tracking requests, please slow down',
|
||||
code: 'GPS_TRACKING_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.canvassGeocodeRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:canvass-geocode:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many geocode requests, please slow down',
|
||||
code: 'CANVASS_GEOCODE_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.adTrackingRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 60, // 60 events/min per IP (generous for scroll-heavy gallery pages)
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:ad-track:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many tracking requests',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.quickJoinRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:quick-join:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many join attempts, please try again later',
|
||||
code: 'QUICK_JOIN_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.authRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 10, // Reduced from 20 to prevent brute force attacks
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:auth:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many authentication attempts, please try again later',
|
||||
code: 'AUTH_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.observabilityRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 20, // 20 requests per minute (stricter than global 500/min)
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:observability:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many observability requests, please try again later',
|
||||
code: 'OBSERVABILITY_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.docsAnalyticsRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 60, // 60 requests/min per IP
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:docs-analytics:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many tracking requests, please slow down',
|
||||
code: 'DOCS_ANALYTICS_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.docsCommentAnonRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:docs-comment-anon:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many anonymous comments, please try again later',
|
||||
code: 'DOCS_COMMENT_ANON_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.docsCommentAuthRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 30,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:docs-comment-auth:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many comments, please try again later',
|
||||
code: 'DOCS_COMMENT_AUTH_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.docsCommentFetchRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 60,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:docs-comment-fetch:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many comment fetch requests, please slow down',
|
||||
code: 'DOCS_COMMENT_FETCH_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.profileViewRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 60, // 60 requests per minute (shared across profile, photo, activity endpoints)
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:profile-view:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many profile requests, please try again later',
|
||||
code: 'PROFILE_VIEW_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.profileEditRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:profile-edit:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many profile edit requests, please try again later',
|
||||
code: 'PROFILE_EDIT_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.profilePhotoRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:profile-photo:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many photo uploads, please try again later',
|
||||
code: 'PROFILE_PHOTO_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.profilePasswordRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // Stricter than auth (10/15min) — profile passwords may be simpler
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:profile-password:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many password attempts, please try again later',
|
||||
code: 'PROFILE_PASSWORD_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.eventSubmissionRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:event-submit:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many event submissions, please try again later',
|
||||
code: 'EVENT_SUBMISSION_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.errorReportRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:error-report:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many error reports, please try again later',
|
||||
code: 'ERROR_REPORT_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
exports.healthMetricsRateLimit = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 30, // 30 requests per minute
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:health-metrics:',
|
||||
}),
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many health check requests, please try again later',
|
||||
code: 'HEALTH_METRICS_RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
},
|
||||
});
|
||||
//# sourceMappingURL=rate-limit.js.map
|
||||
1
api/dist/middleware/rate-limit.js.map
vendored
1
api/dist/middleware/rate-limit.js.map
vendored
File diff suppressed because one or more lines are too long
5
api/dist/middleware/rbac.middleware.d.ts
vendored
5
api/dist/middleware/rbac.middleware.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UserRole } from '@prisma/client';
|
||||
export declare function requireRole(...roles: UserRole[]): (req: Request, _res: Response, next: NextFunction) => void;
|
||||
export declare function requireNonTemp(req: Request, _res: Response, next: NextFunction): void;
|
||||
//# sourceMappingURL=rbac.middleware.d.ts.map
|
||||
1
api/dist/middleware/rbac.middleware.d.ts.map
vendored
1
api/dist/middleware/rbac.middleware.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"rbac.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,IACtC,KAAK,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,YAAY,UAqBzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAY9E"}
|
||||
36
api/dist/middleware/rbac.middleware.js
vendored
36
api/dist/middleware/rbac.middleware.js
vendored
@@ -1,36 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.requireRole = requireRole;
|
||||
exports.requireNonTemp = requireNonTemp;
|
||||
const client_1 = require("@prisma/client");
|
||||
const error_handler_1 = require("./error-handler");
|
||||
function requireRole(...roles) {
|
||||
return (req, _res, next) => {
|
||||
if (!req.user) {
|
||||
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
||||
}
|
||||
// Check multi-role array (falls back to single role via auth middleware)
|
||||
const userRoles = req.user.roles || [req.user.role];
|
||||
// SUPER_ADMIN bypasses all role checks
|
||||
if (userRoles.includes(client_1.UserRole.SUPER_ADMIN)) {
|
||||
return next();
|
||||
}
|
||||
const hasRole = userRoles.some(r => roles.includes(r));
|
||||
if (!hasRole) {
|
||||
throw new error_handler_1.AppError(403, 'Insufficient permissions', 'FORBIDDEN');
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
function requireNonTemp(req, _res, next) {
|
||||
if (!req.user) {
|
||||
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
||||
}
|
||||
const userRoles = req.user.roles || [req.user.role];
|
||||
// User is "temp only" if their only role is TEMP
|
||||
if (userRoles.length === 1 && userRoles[0] === client_1.UserRole.TEMP) {
|
||||
throw new error_handler_1.AppError(403, 'Temporary accounts cannot access this resource', 'TEMP_FORBIDDEN');
|
||||
}
|
||||
next();
|
||||
}
|
||||
//# sourceMappingURL=rbac.middleware.js.map
|
||||
1
api/dist/middleware/rbac.middleware.js.map
vendored
1
api/dist/middleware/rbac.middleware.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":";;AAIA,kCAsBC;AAED,wCAYC;AAvCD,2CAA0C;AAC1C,mDAA2C;AAE3C,SAAgB,WAAW,CAAC,GAAG,KAAiB;IAC9C,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;QACtE,CAAC;QAED,yEAAyE;QACzE,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpD,uCAAuC;QACvC,IAAI,SAAS,CAAC,QAAQ,CAAC,iBAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7C,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAEvD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,WAAW,CAAC,CAAC;QACnE,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC7E,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,iDAAiD;IACjD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,iBAAQ,CAAC,IAAI,EAAE,CAAC;QAC7D,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,gDAAgD,EAAE,gBAAgB,CAAC,CAAC;IAC9F,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
||||
4
api/dist/middleware/validate.d.ts
vendored
4
api/dist/middleware/validate.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodSchema } from 'zod';
|
||||
export declare function validate(schema: ZodSchema, source?: 'body' | 'query' | 'params'): (req: Request, _res: Response, next: NextFunction) => void;
|
||||
//# sourceMappingURL=validate.d.ts.map
|
||||
1
api/dist/middleware/validate.d.ts.map
vendored
1
api/dist/middleware/validate.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/middleware/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAY,MAAM,KAAK,CAAC;AAG1C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,GAAE,MAAM,GAAG,OAAO,GAAG,QAAiB,IAC9E,KAAK,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,YAAY,UAczD"}
|
||||
23
api/dist/middleware/validate.js
vendored
23
api/dist/middleware/validate.js
vendored
@@ -1,23 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.validate = validate;
|
||||
const zod_1 = require("zod");
|
||||
const error_handler_1 = require("./error-handler");
|
||||
function validate(schema, source = 'body') {
|
||||
return (req, _res, next) => {
|
||||
try {
|
||||
const data = schema.parse(req[source]);
|
||||
req[source] = data;
|
||||
next();
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof zod_1.ZodError) {
|
||||
// Sanitize validation errors - only expose field count, not detailed messages
|
||||
const fieldCount = err.errors.length;
|
||||
throw new error_handler_1.AppError(400, `Invalid request data: ${fieldCount} field(s) failed validation`, 'VALIDATION_ERROR');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=validate.js.map
|
||||
1
api/dist/middleware/validate.js.map
vendored
1
api/dist/middleware/validate.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/middleware/validate.ts"],"names":[],"mappings":";;AAIA,4BAeC;AAlBD,6BAA0C;AAC1C,mDAA2C;AAE3C,SAAgB,QAAQ,CAAC,MAAiB,EAAE,SAAsC,MAAM;IACtF,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACvC,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;YACnB,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,cAAQ,EAAE,CAAC;gBAC5B,8EAA8E;gBAC9E,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;gBACrC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,UAAU,6BAA6B,EAAE,kBAAkB,CAAC,CAAC;YAChH,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
||||
3
api/dist/modules/auth/auth.routes.d.ts
vendored
3
api/dist/modules/auth/auth.routes.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export { router as authRouter };
|
||||
//# sourceMappingURL=auth.routes.d.ts.map
|
||||
1
api/dist/modules/auth/auth.routes.d.ts.map
vendored
1
api/dist/modules/auth/auth.routes.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmUxB,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,CAAC"}
|
||||
279
api/dist/modules/auth/auth.routes.js
vendored
279
api/dist/modules/auth/auth.routes.js
vendored
@@ -1,279 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.authRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const zod_1 = require("zod");
|
||||
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||
const client_1 = require("@prisma/client");
|
||||
const auth_service_1 = require("./auth.service");
|
||||
const auth_schemas_1 = require("./auth.schemas");
|
||||
const validate_1 = require("../../middleware/validate");
|
||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||
const rate_limit_1 = require("../../middleware/rate-limit");
|
||||
const database_1 = require("../../config/database");
|
||||
const verification_token_service_1 = require("../../services/verification-token.service");
|
||||
const password_reset_token_service_1 = require("../../services/password-reset-token.service");
|
||||
const email_service_1 = require("../../services/email.service");
|
||||
const settings_service_1 = require("../settings/settings.service");
|
||||
const env_1 = require("../../config/env");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const auth_rate_limits_1 = require("./auth.rate-limits");
|
||||
const profile_service_1 = require("../people/profile.service");
|
||||
const router = (0, express_1.Router)();
|
||||
exports.authRouter = router;
|
||||
// POST /api/auth/login
|
||||
router.post('/login', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.loginSchema), async (req, res, next) => {
|
||||
try {
|
||||
const result = await auth_service_1.authService.login(req.body.email, req.body.password);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/register
|
||||
router.post('/register', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.registerSchema), async (req, res, next) => {
|
||||
try {
|
||||
const result = await auth_service_1.authService.register(req.body);
|
||||
res.status(201).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/verify-email
|
||||
const verifyEmailSchema = zod_1.z.object({ token: zod_1.z.string().min(1) });
|
||||
router.post('/verify-email', rate_limit_1.authRateLimit, (0, validate_1.validate)(verifyEmailSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
const result = await verification_token_service_1.verificationTokenService.verifyToken(token);
|
||||
if (!result.valid || !result.userId) {
|
||||
res.status(400).json({
|
||||
error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const settings = await settings_service_1.siteSettingsService.get();
|
||||
const autoApprove = settings.autoApproveVerifiedUsers;
|
||||
const newStatus = autoApprove ? client_1.UserStatus.ACTIVE : client_1.UserStatus.PENDING_APPROVAL;
|
||||
await database_1.prisma.user.update({
|
||||
where: { id: result.userId },
|
||||
data: { emailVerified: true, status: newStatus },
|
||||
});
|
||||
// If not auto-approved, notify admins
|
||||
if (!autoApprove) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: result.userId } });
|
||||
if (user) {
|
||||
const admins = await database_1.prisma.user.findMany({
|
||||
where: { role: client_1.UserRole.SUPER_ADMIN, status: client_1.UserStatus.ACTIVE },
|
||||
select: { email: true },
|
||||
});
|
||||
if (admins.length > 0) {
|
||||
await email_service_1.emailService.sendPendingApprovalNotification({
|
||||
adminEmails: admins.map(a => a.email),
|
||||
newUserEmail: user.email,
|
||||
newUserName: user.name || '',
|
||||
}).catch(err => logger_1.logger.error('Failed to send approval notification:', err));
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json({
|
||||
verified: true,
|
||||
approved: autoApprove,
|
||||
message: autoApprove
|
||||
? 'Email verified. You can now log in.'
|
||||
: 'Email verified. Your account is pending admin approval.',
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/resend-verification
|
||||
const resendVerificationSchema = zod_1.z.object({ email: zod_1.z.string().email() });
|
||||
router.post('/resend-verification', (0, auth_rate_limits_1.createVerificationRateLimit)(), (0, validate_1.validate)(resendVerificationSchema), async (req, res, next) => {
|
||||
try {
|
||||
// Always return success to prevent user enumeration
|
||||
res.json({ message: 'If your email is registered and pending verification, a new verification link has been sent.' });
|
||||
// Send asynchronously (don't block response)
|
||||
const user = await database_1.prisma.user.findUnique({ where: { email: req.body.email } });
|
||||
if (user && user.status === client_1.UserStatus.PENDING_VERIFICATION) {
|
||||
const token = await verification_token_service_1.verificationTokenService.createToken(user.id);
|
||||
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||
const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
|
||||
await email_service_1.emailService.sendVerificationEmail({
|
||||
recipientEmail: user.email,
|
||||
recipientName: user.name || 'there',
|
||||
verificationUrl,
|
||||
}).catch(err => logger_1.logger.error('Failed to resend verification email:', err));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/forgot-password
|
||||
const forgotPasswordSchema = zod_1.z.object({ email: zod_1.z.string().email() });
|
||||
router.post('/forgot-password', (0, auth_rate_limits_1.createResetRateLimit)(), (0, validate_1.validate)(forgotPasswordSchema), async (req, res, next) => {
|
||||
try {
|
||||
// Always return success to prevent user enumeration
|
||||
res.json({ message: 'If your email is registered, a password reset link has been sent.' });
|
||||
// Send asynchronously
|
||||
const user = await database_1.prisma.user.findUnique({ where: { email: req.body.email } });
|
||||
if (user && user.status === client_1.UserStatus.ACTIVE) {
|
||||
const token = await password_reset_token_service_1.passwordResetTokenService.createToken(user.id);
|
||||
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||
const resetUrl = `${adminUrl}/reset-password?token=${token}`;
|
||||
await email_service_1.emailService.sendPasswordResetEmail({
|
||||
recipientEmail: user.email,
|
||||
recipientName: user.name || 'there',
|
||||
resetUrl,
|
||||
}).catch(err => logger_1.logger.error('Failed to send password reset email:', err));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/reset-password
|
||||
const resetPasswordSchema = zod_1.z.object({
|
||||
token: zod_1.z.string().min(1),
|
||||
password: zod_1.z.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one digit'),
|
||||
});
|
||||
router.post('/reset-password', rate_limit_1.authRateLimit, (0, validate_1.validate)(resetPasswordSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { token, password } = req.body;
|
||||
const result = await password_reset_token_service_1.passwordResetTokenService.validateToken(token);
|
||||
if (!result.valid || !result.userId) {
|
||||
res.status(400).json({
|
||||
error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const hashedPassword = await bcryptjs_1.default.hash(password, 12);
|
||||
// Update password, mark token used, invalidate all refresh tokens — all in one transaction
|
||||
await database_1.prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: { id: result.userId },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
await tx.refreshToken.deleteMany({ where: { userId: result.userId } });
|
||||
await tx.passwordResetToken.update({
|
||||
where: { token },
|
||||
data: { usedAt: new Date() },
|
||||
});
|
||||
});
|
||||
res.json({ message: 'Password has been reset. You can now log in with your new password.' });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/refresh
|
||||
router.post('/refresh', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
|
||||
try {
|
||||
const result = await auth_service_1.authService.refreshTokens(req.body.refreshToken);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/auth/logout
|
||||
router.post('/logout', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
|
||||
try {
|
||||
await auth_service_1.authService.logout(req.body.refreshToken);
|
||||
res.json({ message: 'Logged out' });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/auth/me/profile-token
|
||||
router.get('/me/profile-token', auth_middleware_1.authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
// Look up existing Contact linked to this user
|
||||
let contact = await database_1.prisma.contact.findUnique({ where: { userId } });
|
||||
// Auto-create Contact if none exists
|
||||
if (!contact) {
|
||||
const user = await database_1.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { name: true, email: true },
|
||||
});
|
||||
if (!user) {
|
||||
res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
contact = await database_1.prisma.contact.create({
|
||||
data: {
|
||||
displayName: user.name || user.email,
|
||||
firstName: user.name?.split(' ')[0] || null,
|
||||
lastName: user.name?.split(' ').slice(1).join(' ') || null,
|
||||
email: user.email,
|
||||
userId,
|
||||
primarySource: 'USER',
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
// Race condition: another request created the Contact — retry lookup
|
||||
if (err.code === 'P2002') {
|
||||
contact = await database_1.prisma.contact.findUnique({ where: { userId } });
|
||||
}
|
||||
if (!contact)
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// Generate profile token if Contact doesn't have one
|
||||
if (!contact.profileToken) {
|
||||
const result = await profile_service_1.profileService.generateProfileToken(contact.id);
|
||||
res.json({ token: result.token });
|
||||
return;
|
||||
}
|
||||
res.json({ token: contact.profileToken });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/auth/me
|
||||
router.get('/me', auth_middleware_1.authenticate, async (req, res, next) => {
|
||||
try {
|
||||
const user = await database_1.prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
role: true,
|
||||
roles: true,
|
||||
status: true,
|
||||
permissions: true,
|
||||
createdVia: true,
|
||||
emailVerified: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
|
||||
return;
|
||||
}
|
||||
res.json(user);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=auth.routes.js.map
|
||||
1
api/dist/modules/auth/auth.routes.js.map
vendored
1
api/dist/modules/auth/auth.routes.js.map
vendored
File diff suppressed because one or more lines are too long
41
api/dist/modules/auth/auth.schemas.d.ts
vendored
41
api/dist/modules/auth/auth.schemas.d.ts
vendored
@@ -1,41 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
export declare const loginSchema: z.ZodObject<{
|
||||
email: z.ZodString;
|
||||
password: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
email: string;
|
||||
password: string;
|
||||
}, {
|
||||
email: string;
|
||||
password: string;
|
||||
}>;
|
||||
export declare const registerSchema: z.ZodObject<{
|
||||
email: z.ZodString;
|
||||
password: z.ZodString;
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
phone: z.ZodOptional<z.ZodString>;
|
||||
inviteCode: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
email: string;
|
||||
password: string;
|
||||
name?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
inviteCode?: string | undefined;
|
||||
}, {
|
||||
email: string;
|
||||
password: string;
|
||||
name?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
inviteCode?: string | undefined;
|
||||
}>;
|
||||
export declare const refreshSchema: z.ZodObject<{
|
||||
refreshToken: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
refreshToken: string;
|
||||
}, {
|
||||
refreshToken: string;
|
||||
}>;
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||
export type RefreshInput = z.infer<typeof refreshSchema>;
|
||||
//# sourceMappingURL=auth.schemas.d.ts.map
|
||||
1
api/dist/modules/auth/auth.schemas.d.ts.map
vendored
1
api/dist/modules/auth/auth.schemas.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;EAWzB,CAAC;AAEH,eAAO,MAAM,aAAa;;;;;;EAExB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACrD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
||||
24
api/dist/modules/auth/auth.schemas.js
vendored
24
api/dist/modules/auth/auth.schemas.js
vendored
@@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.refreshSchema = exports.registerSchema = exports.loginSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
exports.loginSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email(),
|
||||
password: zod_1.z.string().min(1, 'Password is required'),
|
||||
});
|
||||
exports.registerSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email(),
|
||||
password: zod_1.z.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one digit'),
|
||||
name: zod_1.z.string().optional(),
|
||||
phone: zod_1.z.string().optional(),
|
||||
inviteCode: zod_1.z.string().max(20).optional(),
|
||||
// Role removed from public registration - must be set server-side only
|
||||
});
|
||||
exports.refreshSchema = zod_1.z.object({
|
||||
refreshToken: zod_1.z.string().min(1, 'Refresh token is required'),
|
||||
});
|
||||
//# sourceMappingURL=auth.schemas.js.map
|
||||
1
api/dist/modules/auth/auth.schemas.js.map
vendored
1
api/dist/modules/auth/auth.schemas.js.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.schemas.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAGX,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;CACpD,CAAC,CAAC;AAEU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,EAAE,EAAE,yCAAyC,CAAC;SAClD,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,0CAA0C,CAAC;IAC7D,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzC,uEAAuE;CACxE,CAAC,CAAC;AAEU,QAAA,aAAa,GAAG,OAAC,CAAC,MAAM,CAAC;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;CAC7D,CAAC,CAAC"}
|
||||
109
api/dist/modules/auth/auth.service.d.ts
vendored
109
api/dist/modules/auth/auth.service.d.ts
vendored
@@ -1,109 +0,0 @@
|
||||
import { UserRole } from '@prisma/client';
|
||||
import type { RegisterInput } from './auth.schemas';
|
||||
export interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
type UserForToken = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
roles?: unknown;
|
||||
};
|
||||
export declare const authService: {
|
||||
login(email: string, password: string): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: {
|
||||
status: import(".prisma/client").$Enums.UserStatus;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
pronouns: string | null;
|
||||
role: import(".prisma/client").$Enums.UserRole;
|
||||
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||
expiresAt: Date | null;
|
||||
expireDays: number | null;
|
||||
lastLoginAt: Date | null;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
}>;
|
||||
register(data: RegisterInput): Promise<{
|
||||
user: {
|
||||
status: import(".prisma/client").$Enums.UserStatus;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
pronouns: string | null;
|
||||
role: import(".prisma/client").$Enums.UserRole;
|
||||
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||
expiresAt: Date | null;
|
||||
expireDays: number | null;
|
||||
lastLoginAt: Date | null;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
requiresVerification: boolean;
|
||||
message: string;
|
||||
} | {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: {
|
||||
status: import(".prisma/client").$Enums.UserStatus;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
pronouns: string | null;
|
||||
role: import(".prisma/client").$Enums.UserRole;
|
||||
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||
expiresAt: Date | null;
|
||||
expireDays: number | null;
|
||||
lastLoginAt: Date | null;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
requiresVerification?: undefined;
|
||||
message?: undefined;
|
||||
}>;
|
||||
refreshTokens(refreshToken: string): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: {
|
||||
status: import(".prisma/client").$Enums.UserStatus;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
pronouns: string | null;
|
||||
role: import(".prisma/client").$Enums.UserRole;
|
||||
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||
expiresAt: Date | null;
|
||||
expireDays: number | null;
|
||||
lastLoginAt: Date | null;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
}>;
|
||||
logout(refreshToken: string): Promise<void>;
|
||||
generateAccessToken(user: UserForToken): string;
|
||||
generateRefreshToken(user: UserForToken): Promise<string>;
|
||||
generateTokenPair(user: UserForToken): Promise<TokenPair>;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=auth.service.d.ts.map
|
||||
1
api/dist/modules/auth/auth.service.d.ts.map
vendored
1
api/dist/modules/auth/auth.service.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAUtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AASpD,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,KAAK,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAUnF,eAAO,MAAM,WAAW;iBACH,MAAM,YAAY,MAAM;qBAf9B,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;;;mBA+EC,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAhFrB,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;;;;;gCAiLc,MAAM;;;;;;;;;;;;;;;;;;;;;;yBAsEb,MAAM;8BAIP,YAAY,GAAG,MAAM;+BAcd,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;4BA2BjC,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;CAKhE,CAAC"}
|
||||
305
api/dist/modules/auth/auth.service.js
vendored
305
api/dist/modules/auth/auth.service.js
vendored
@@ -1,305 +0,0 @@
|
||||
"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.authService = void 0;
|
||||
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const client_1 = require("@prisma/client");
|
||||
const database_1 = require("../../config/database");
|
||||
const env_1 = require("../../config/env");
|
||||
const error_handler_1 = require("../../middleware/error-handler");
|
||||
const metrics_1 = require("../../utils/metrics");
|
||||
const settings_service_1 = require("../settings/settings.service");
|
||||
const verification_token_service_1 = require("../../services/verification-token.service");
|
||||
const email_service_1 = require("../../services/email.service");
|
||||
const roles_1 = require("../../utils/roles");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
/** Parse the roles JSON field into a UserRole[] array */
|
||||
function parseRoles(user) {
|
||||
if (Array.isArray(user.roles) && user.roles.length > 0) {
|
||||
return user.roles;
|
||||
}
|
||||
return [user.role];
|
||||
}
|
||||
exports.authService = {
|
||||
async login(email, password) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
|
||||
}
|
||||
const valid = await bcryptjs_1.default.compare(password, user.password);
|
||||
if (!valid) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
|
||||
}
|
||||
// Status-specific errors
|
||||
if (user.status === client_1.UserStatus.PENDING_VERIFICATION) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(403, 'Please verify your email address before logging in', 'EMAIL_NOT_VERIFIED');
|
||||
}
|
||||
if (user.status === client_1.UserStatus.PENDING_APPROVAL) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(403, 'Your account is pending admin approval', 'ACCOUNT_PENDING');
|
||||
}
|
||||
if (user.status !== client_1.UserStatus.ACTIVE) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(403, `Account is ${user.status.toLowerCase()}`, 'ACCOUNT_INACTIVE');
|
||||
}
|
||||
if (user.expiresAt && user.expiresAt < new Date()) {
|
||||
(0, metrics_1.recordLoginAttempt)('failure');
|
||||
throw new error_handler_1.AppError(403, 'Account has expired', 'ACCOUNT_EXPIRED');
|
||||
}
|
||||
(0, metrics_1.recordLoginAttempt)('success');
|
||||
await database_1.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
// Fire-and-forget: log USER_LOGIN activity on linked Contact
|
||||
database_1.prisma.contact.findFirst({
|
||||
where: { userId: user.id, mergedIntoId: null },
|
||||
}).then(async (contact) => {
|
||||
if (contact) {
|
||||
await database_1.prisma.contactActivity.create({
|
||||
data: {
|
||||
contactId: contact.id,
|
||||
type: 'USER_LOGIN',
|
||||
title: 'User logged in',
|
||||
description: `Login from ${user.email}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
logger_1.logger.warn('Login activity logging failed:', err);
|
||||
});
|
||||
const tokens = await this.generateTokenPair(user);
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
return { user: userWithoutPassword, ...tokens };
|
||||
},
|
||||
async register(data) {
|
||||
// Check if public registration is enabled
|
||||
const settings = await settings_service_1.siteSettingsService.get();
|
||||
if (!settings.enablePublicRegistration) {
|
||||
throw new error_handler_1.AppError(403, 'Public registration is currently disabled', 'REGISTRATION_DISABLED');
|
||||
}
|
||||
const existing = await database_1.prisma.user.findUnique({ where: { email: data.email } });
|
||||
if (existing) {
|
||||
throw new error_handler_1.AppError(409, 'Email already registered', 'EMAIL_EXISTS');
|
||||
}
|
||||
const hashedPassword = await bcryptjs_1.default.hash(data.password, 12);
|
||||
// Determine if email verification is needed
|
||||
const smtpReady = await email_service_1.emailService.isSmtpConfigured();
|
||||
const requireVerification = settings.enableEmailVerification && smtpReady;
|
||||
const user = await database_1.prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
role: client_1.UserRole.USER,
|
||||
roles: JSON.parse(JSON.stringify([client_1.UserRole.USER])),
|
||||
status: requireVerification ? client_1.UserStatus.PENDING_VERIFICATION : client_1.UserStatus.ACTIVE,
|
||||
emailVerified: !requireVerification,
|
||||
createdVia: 'SELF_REGISTRATION',
|
||||
},
|
||||
});
|
||||
// Fire-and-forget: process referral if invite code provided
|
||||
if (data.inviteCode) {
|
||||
Promise.resolve().then(() => __importStar(require('../social/referral.service'))).then(({ referralService }) => {
|
||||
referralService.processRegistrationReferral(user.id, data.inviteCode).catch(err => {
|
||||
logger_1.logger.warn('Referral processing failed:', err);
|
||||
});
|
||||
}).catch(() => { });
|
||||
}
|
||||
// Fire-and-forget: auto-link or create Contact if People feature is enabled
|
||||
settings_service_1.siteSettingsService.get().then(async (s) => {
|
||||
if (!s.enablePeople)
|
||||
return;
|
||||
// Check for existing Contact with matching email → link
|
||||
const existingContact = await database_1.prisma.contact.findFirst({
|
||||
where: { email: { equals: data.email, mode: 'insensitive' }, userId: null, mergedIntoId: null },
|
||||
});
|
||||
if (existingContact) {
|
||||
await database_1.prisma.contact.update({ where: { id: existingContact.id }, data: { userId: user.id } });
|
||||
logger_1.logger.info(`Auto-linked contact ${existingContact.id} to registered user ${user.id}`);
|
||||
}
|
||||
else {
|
||||
// Create new Contact linked to the user
|
||||
await database_1.prisma.contact.create({
|
||||
data: {
|
||||
displayName: data.name || data.email,
|
||||
firstName: data.name?.split(' ')[0] || null,
|
||||
lastName: data.name?.split(' ').slice(1).join(' ') || null,
|
||||
email: data.email,
|
||||
phone: data.phone || null,
|
||||
primarySource: 'USER',
|
||||
userId: user.id,
|
||||
tags: [],
|
||||
},
|
||||
});
|
||||
logger_1.logger.info(`Auto-created contact for registered user ${user.id}`);
|
||||
}
|
||||
}).catch(err => {
|
||||
logger_1.logger.warn('Contact auto-creation on register failed:', err);
|
||||
});
|
||||
// If verification required, send email and don't issue tokens
|
||||
if (requireVerification) {
|
||||
const token = await verification_token_service_1.verificationTokenService.createToken(user.id);
|
||||
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||
const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
|
||||
await email_service_1.emailService.sendVerificationEmail({
|
||||
recipientEmail: user.email,
|
||||
recipientName: user.name || 'there',
|
||||
verificationUrl,
|
||||
});
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
return {
|
||||
user: userWithoutPassword,
|
||||
requiresVerification: true,
|
||||
message: 'Please check your email to verify your account',
|
||||
};
|
||||
}
|
||||
// No verification needed — issue tokens immediately
|
||||
const tokens = await this.generateTokenPair(user);
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
return { user: userWithoutPassword, ...tokens };
|
||||
},
|
||||
async refreshTokens(refreshToken) {
|
||||
let payload;
|
||||
try {
|
||||
payload = jsonwebtoken_1.default.verify(refreshToken, env_1.env.JWT_REFRESH_SECRET, { algorithms: ['HS256'] });
|
||||
}
|
||||
catch {
|
||||
throw new error_handler_1.AppError(401, 'Invalid refresh token', 'INVALID_REFRESH_TOKEN');
|
||||
}
|
||||
const stored = await database_1.prisma.refreshToken.findUnique({
|
||||
where: { token: refreshToken },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!stored) {
|
||||
throw new error_handler_1.AppError(401, 'Refresh token not found', 'INVALID_REFRESH_TOKEN');
|
||||
}
|
||||
// Check user status — banned/inactive users must not get new tokens
|
||||
if (stored.user.status !== client_1.UserStatus.ACTIVE) {
|
||||
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||
throw new error_handler_1.AppError(401, 'Account is not active', 'ACCOUNT_INACTIVE');
|
||||
}
|
||||
// Check account expiry
|
||||
if (stored.user.expiresAt && stored.user.expiresAt < new Date()) {
|
||||
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||
throw new error_handler_1.AppError(401, 'Account has expired', 'ACCOUNT_EXPIRED');
|
||||
}
|
||||
if (stored.expiresAt < new Date()) {
|
||||
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||
throw new error_handler_1.AppError(401, 'Refresh token expired', 'REFRESH_TOKEN_EXPIRED');
|
||||
}
|
||||
// Rotate: delete old and create new atomically
|
||||
const tokens = await database_1.prisma.$transaction(async (tx) => {
|
||||
await tx.refreshToken.delete({ where: { id: stored.id } });
|
||||
const userRoles = parseRoles(stored.user);
|
||||
const accessToken = this.generateAccessToken(stored.user);
|
||||
const refreshPayload = {
|
||||
id: stored.user.id,
|
||||
email: stored.user.email,
|
||||
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||
roles: userRoles,
|
||||
};
|
||||
const refreshToken = jsonwebtoken_1.default.sign(refreshPayload, env_1.env.JWT_REFRESH_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
||||
});
|
||||
const decoded = jsonwebtoken_1.default.decode(refreshToken);
|
||||
const expiresAt = new Date(decoded.exp * 1000);
|
||||
await tx.refreshToken.create({
|
||||
data: {
|
||||
token: refreshToken,
|
||||
userId: stored.user.id,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
return { accessToken, refreshToken };
|
||||
});
|
||||
const { password: _, ...userWithoutPassword } = stored.user;
|
||||
return { user: userWithoutPassword, ...tokens };
|
||||
},
|
||||
async logout(refreshToken) {
|
||||
await database_1.prisma.refreshToken.deleteMany({ where: { token: refreshToken } });
|
||||
},
|
||||
generateAccessToken(user) {
|
||||
const userRoles = parseRoles(user);
|
||||
const payload = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||
roles: userRoles,
|
||||
};
|
||||
return jsonwebtoken_1.default.sign(payload, env_1.env.JWT_ACCESS_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: env_1.env.JWT_ACCESS_EXPIRY,
|
||||
});
|
||||
},
|
||||
async generateRefreshToken(user) {
|
||||
const userRoles = parseRoles(user);
|
||||
const payload = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||
roles: userRoles,
|
||||
};
|
||||
const token = jsonwebtoken_1.default.sign(payload, env_1.env.JWT_REFRESH_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
||||
});
|
||||
const decoded = jsonwebtoken_1.default.decode(token);
|
||||
const expiresAt = new Date(decoded.exp * 1000);
|
||||
await database_1.prisma.refreshToken.create({
|
||||
data: {
|
||||
token,
|
||||
userId: user.id,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
return token;
|
||||
},
|
||||
async generateTokenPair(user) {
|
||||
const accessToken = this.generateAccessToken(user);
|
||||
const refreshToken = await this.generateRefreshToken(user);
|
||||
return { accessToken, refreshToken };
|
||||
},
|
||||
};
|
||||
//# sourceMappingURL=auth.service.js.map
|
||||
1
api/dist/modules/auth/auth.service.js.map
vendored
1
api/dist/modules/auth/auth.service.js.map
vendored
File diff suppressed because one or more lines are too long
54
api/dist/modules/docs/docs-files.service.d.ts
vendored
54
api/dist/modules/docs/docs-files.service.d.ts
vendored
@@ -1,54 +0,0 @@
|
||||
export interface FileNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileNode[];
|
||||
}
|
||||
/**
|
||||
* Resolve and validate a relative path within MKDOCS_DOCS_PATH.
|
||||
* Throws if the resolved path escapes the docs root.
|
||||
*/
|
||||
declare function safeResolve(relativePath: string): string;
|
||||
export declare class PathTraversalError extends Error {
|
||||
constructor();
|
||||
}
|
||||
export declare class FileNotFoundError extends Error {
|
||||
constructor(filePath: string);
|
||||
}
|
||||
/**
|
||||
* Recursively list the file tree under MKDOCS_DOCS_PATH.
|
||||
* Cached in Redis with 1-hour TTL for root calls.
|
||||
*/
|
||||
declare function listTree(dir?: string, relBase?: string): Promise<FileNode[]>;
|
||||
declare function readFileContent(relativePath: string): Promise<string>;
|
||||
declare function writeFileContent(relativePath: string, content: string): Promise<void>;
|
||||
declare function createFile(relativePath: string, content?: string, isDirectory?: boolean): Promise<void>;
|
||||
declare function deleteFile(relativePath: string): Promise<void>;
|
||||
declare function renameFile(fromPath: string, toPath: string): Promise<void>;
|
||||
declare function isEditableFile(relativePath: string): boolean;
|
||||
declare function uploadFile(relativePath: string, sourcePath: string): Promise<void>;
|
||||
declare function invalidateTreeCache(): Promise<void>;
|
||||
/**
|
||||
* Flatten a FileNode tree into file-only entries, then filter by
|
||||
* case-insensitive query match on name or path. Name matches score
|
||||
* higher so they sort first.
|
||||
*/
|
||||
declare function searchFiles(query: string, limit?: number): Promise<{
|
||||
name: string;
|
||||
path: string;
|
||||
}[]>;
|
||||
export declare const docsFilesService: {
|
||||
listTree: typeof listTree;
|
||||
readFileContent: typeof readFileContent;
|
||||
writeFileContent: typeof writeFileContent;
|
||||
createFile: typeof createFile;
|
||||
deleteFile: typeof deleteFile;
|
||||
renameFile: typeof renameFile;
|
||||
uploadFile: typeof uploadFile;
|
||||
safeResolve: typeof safeResolve;
|
||||
isEditableFile: typeof isEditableFile;
|
||||
invalidateTreeCache: typeof invalidateTreeCache;
|
||||
searchFiles: typeof searchFiles;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=docs-files.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"docs-files.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs-files.service.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAcD;;;GAGG;AACH,iBAAS,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,QAAQ,EAAE,MAAM;CAI7B;AAED;;;GAGG;AACH,iBAAe,QAAQ,CAAC,GAAG,GAAE,MAAkB,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiD1F;AAED,iBAAe,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCpE;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtG;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7D;AAED,iBAAe,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAED,iBAAS,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAGrD;AAOD,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjF;AAED,iBAAe,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlD;AAED;;;;GAIG;AACH,iBAAe,WAAW,CACxB,KAAK,EAAE,MAAM,EACb,KAAK,SAAI,GACR,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAyB3C;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;CAY5B,CAAC"}
|
||||
312
api/dist/modules/docs/docs-files.service.js
vendored
312
api/dist/modules/docs/docs-files.service.js
vendored
@@ -1,312 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.docsFilesService = exports.FileNotFoundError = exports.PathTraversalError = void 0;
|
||||
const promises_1 = require("fs/promises");
|
||||
const path_1 = require("path");
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const env_1 = require("../../config/env");
|
||||
const redis_1 = require("../../config/redis");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const metrics_1 = require("../../utils/metrics");
|
||||
const DOCS_ROOT = (0, path_1.resolve)(env_1.env.MKDOCS_DOCS_PATH);
|
||||
// Redis cache configuration
|
||||
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
||||
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
||||
const TREE_CACHE_TTL = 30; // 30 seconds — short so external changes show quickly
|
||||
const FILE_CONTENT_CACHE_TTL = 60 * 60; // 1 hour for file content
|
||||
function hashFilePath(path) {
|
||||
return crypto_1.default.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
||||
}
|
||||
/**
|
||||
* Resolve and validate a relative path within MKDOCS_DOCS_PATH.
|
||||
* Throws if the resolved path escapes the docs root.
|
||||
*/
|
||||
function safeResolve(relativePath) {
|
||||
const normalized = (0, path_1.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||
const resolved = (0, path_1.resolve)(DOCS_ROOT, normalized);
|
||||
// Use DOCS_ROOT + sep to prevent prefix attacks (e.g., /mkdocs/docs-evil matching /mkdocs/docs)
|
||||
if (resolved !== DOCS_ROOT && !resolved.startsWith(DOCS_ROOT + '/')) {
|
||||
throw new PathTraversalError();
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
class PathTraversalError extends Error {
|
||||
constructor() {
|
||||
super('Path traversal not allowed');
|
||||
this.name = 'PathTraversalError';
|
||||
}
|
||||
}
|
||||
exports.PathTraversalError = PathTraversalError;
|
||||
class FileNotFoundError extends Error {
|
||||
constructor(filePath) {
|
||||
super(`File not found: ${filePath}`);
|
||||
this.name = 'FileNotFoundError';
|
||||
}
|
||||
}
|
||||
exports.FileNotFoundError = FileNotFoundError;
|
||||
/**
|
||||
* Recursively list the file tree under MKDOCS_DOCS_PATH.
|
||||
* Cached in Redis with 1-hour TTL for root calls.
|
||||
*/
|
||||
async function listTree(dir = DOCS_ROOT, relBase = '') {
|
||||
// Try cache for root call only
|
||||
if (dir === DOCS_ROOT && !relBase) {
|
||||
try {
|
||||
const cached = await redis_1.redis.get(TREE_CACHE_KEY);
|
||||
if (cached) {
|
||||
metrics_1.cm_docs_cache_hits.inc({ type: 'tree' });
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
metrics_1.cm_docs_cache_misses.inc({ type: 'tree' });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to get cached docs tree:', err);
|
||||
metrics_1.cm_docs_cache_misses.inc({ type: 'tree' });
|
||||
}
|
||||
}
|
||||
const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
||||
const sorted = entries
|
||||
.filter(e => !e.name.startsWith('.'))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory() && !b.isDirectory())
|
||||
return -1;
|
||||
if (!a.isDirectory() && b.isDirectory())
|
||||
return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
// Parallel I/O instead of sequential for better performance
|
||||
const nodes = await Promise.all(sorted.map(async (entry) => {
|
||||
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
const children = await listTree((0, path_1.join)(dir, entry.name), relPath);
|
||||
return { name: entry.name, path: relPath, isDirectory: true, children };
|
||||
}
|
||||
else {
|
||||
return { name: entry.name, path: relPath, isDirectory: false };
|
||||
}
|
||||
}));
|
||||
// Cache root result
|
||||
if (dir === DOCS_ROOT && !relBase) {
|
||||
try {
|
||||
await redis_1.redis.setex(TREE_CACHE_KEY, TREE_CACHE_TTL, JSON.stringify(nodes));
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to cache docs tree:', err);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
async function readFileContent(relativePath) {
|
||||
const cacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;
|
||||
// Try cache first
|
||||
try {
|
||||
const cached = await redis_1.redis.get(cacheKey);
|
||||
if (cached) {
|
||||
metrics_1.cm_docs_cache_hits.inc({ type: 'file' });
|
||||
return cached;
|
||||
}
|
||||
metrics_1.cm_docs_cache_misses.inc({ type: 'file' });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to get cached file content:', err);
|
||||
metrics_1.cm_docs_cache_misses.inc({ type: 'file' });
|
||||
}
|
||||
// Read from disk
|
||||
const fullPath = safeResolve(relativePath);
|
||||
try {
|
||||
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
||||
// Cache the result
|
||||
try {
|
||||
await redis_1.redis.setex(cacheKey, FILE_CONTENT_CACHE_TTL, content);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to cache file content:', err);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new FileNotFoundError(relativePath);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async function writeFileContent(relativePath, content) {
|
||||
const fullPath = safeResolve(relativePath);
|
||||
await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
|
||||
await (0, promises_1.writeFile)(fullPath, content, 'utf-8');
|
||||
// Invalidate file cache (content changed, structure unchanged)
|
||||
const cacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;
|
||||
try {
|
||||
await redis_1.redis.del(cacheKey);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate file cache:', err);
|
||||
}
|
||||
}
|
||||
async function createFile(relativePath, content, isDirectory) {
|
||||
const fullPath = safeResolve(relativePath);
|
||||
try {
|
||||
await (0, promises_1.stat)(fullPath);
|
||||
throw new Error(`Already exists: ${relativePath}`);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
if (err.message?.startsWith('Already exists'))
|
||||
throw err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (isDirectory) {
|
||||
await (0, promises_1.mkdir)(fullPath, { recursive: true });
|
||||
}
|
||||
else {
|
||||
await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
|
||||
await (0, promises_1.writeFile)(fullPath, content || '', 'utf-8');
|
||||
}
|
||||
// Invalidate tree cache (structure changed)
|
||||
try {
|
||||
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate tree cache:', err);
|
||||
}
|
||||
}
|
||||
async function deleteFile(relativePath) {
|
||||
const fullPath = safeResolve(relativePath);
|
||||
try {
|
||||
const info = await (0, promises_1.stat)(fullPath);
|
||||
if (info.isDirectory()) {
|
||||
const entries = await (0, promises_1.readdir)(fullPath);
|
||||
if (entries.length > 0) {
|
||||
throw new Error('Directory is not empty');
|
||||
}
|
||||
await (0, promises_1.rm)(fullPath, { recursive: false });
|
||||
}
|
||||
else {
|
||||
await (0, promises_1.rm)(fullPath);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new FileNotFoundError(relativePath);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
// Invalidate both tree and file cache
|
||||
const fileCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;
|
||||
try {
|
||||
await Promise.all([
|
||||
redis_1.redis.del(TREE_CACHE_KEY),
|
||||
redis_1.redis.del(fileCacheKey),
|
||||
]);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate caches on delete:', err);
|
||||
}
|
||||
}
|
||||
async function renameFile(fromPath, toPath) {
|
||||
const fullFrom = safeResolve(fromPath);
|
||||
const fullTo = safeResolve(toPath);
|
||||
try {
|
||||
await (0, promises_1.stat)(fullFrom);
|
||||
}
|
||||
catch {
|
||||
throw new FileNotFoundError(fromPath);
|
||||
}
|
||||
await (0, promises_1.mkdir)((0, path_1.dirname)(fullTo), { recursive: true });
|
||||
await (0, promises_1.rename)(fullFrom, fullTo);
|
||||
// Invalidate tree and both file paths
|
||||
const fromCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(fromPath)}`;
|
||||
const toCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(toPath)}`;
|
||||
try {
|
||||
await Promise.all([
|
||||
redis_1.redis.del(TREE_CACHE_KEY),
|
||||
redis_1.redis.del(fromCacheKey),
|
||||
redis_1.redis.del(toCacheKey),
|
||||
]);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate caches on rename:', err);
|
||||
}
|
||||
}
|
||||
function isEditableFile(relativePath) {
|
||||
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
||||
}
|
||||
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||
'.pdf', '.zip',
|
||||
]);
|
||||
async function uploadFile(relativePath, sourcePath) {
|
||||
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||
if (!ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||
throw new Error(`File type not allowed: ${ext}`);
|
||||
}
|
||||
const fullPath = safeResolve(relativePath);
|
||||
await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
|
||||
await (0, promises_1.copyFile)(sourcePath, fullPath);
|
||||
// Invalidate tree cache (structure changed)
|
||||
try {
|
||||
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate tree cache after upload:', err);
|
||||
}
|
||||
}
|
||||
async function invalidateTreeCache() {
|
||||
try {
|
||||
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.warn('Failed to invalidate tree cache:', err);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Flatten a FileNode tree into file-only entries, then filter by
|
||||
* case-insensitive query match on name or path. Name matches score
|
||||
* higher so they sort first.
|
||||
*/
|
||||
async function searchFiles(query, limit = 5) {
|
||||
const tree = await listTree();
|
||||
const q = query.toLowerCase();
|
||||
const matches = [];
|
||||
function walk(nodes) {
|
||||
for (const node of nodes) {
|
||||
if (node.isDirectory) {
|
||||
if (node.children)
|
||||
walk(node.children);
|
||||
}
|
||||
else {
|
||||
const nameLower = node.name.toLowerCase();
|
||||
const pathLower = node.path.toLowerCase();
|
||||
if (nameLower.includes(q)) {
|
||||
matches.push({ name: node.name, path: node.path, score: 2 });
|
||||
}
|
||||
else if (pathLower.includes(q)) {
|
||||
matches.push({ name: node.name, path: node.path, score: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(tree);
|
||||
matches.sort((a, b) => b.score - a.score);
|
||||
return matches.slice(0, limit).map(({ name, path }) => ({ name, path }));
|
||||
}
|
||||
exports.docsFilesService = {
|
||||
listTree,
|
||||
readFileContent,
|
||||
writeFileContent,
|
||||
createFile,
|
||||
deleteFile,
|
||||
renameFile,
|
||||
uploadFile,
|
||||
safeResolve,
|
||||
isEditableFile,
|
||||
invalidateTreeCache,
|
||||
searchFiles,
|
||||
};
|
||||
//# sourceMappingURL=docs-files.service.js.map
|
||||
File diff suppressed because one or more lines are too long
2
api/dist/modules/docs/docs.routes.d.ts
vendored
2
api/dist/modules/docs/docs.routes.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export declare const docsRouter: import("express-serve-static-core").Router;
|
||||
//# sourceMappingURL=docs.routes.d.ts.map
|
||||
1
api/dist/modules/docs/docs.routes.d.ts.map
vendored
1
api/dist/modules/docs/docs.routes.d.ts.map
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"docs.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs.routes.ts"],"names":[],"mappings":"AA4YA,eAAO,MAAM,UAAU,4CAAS,CAAC"}
|
||||
336
api/dist/modules/docs/docs.routes.js
vendored
336
api/dist/modules/docs/docs.routes.js
vendored
@@ -1,336 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.docsRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const multer_1 = __importDefault(require("multer"));
|
||||
const promises_1 = require("fs/promises");
|
||||
const path_1 = require("path");
|
||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||
const env_1 = require("../../config/env");
|
||||
const roles_1 = require("../../utils/roles");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const health_check_1 = require("../../utils/health-check");
|
||||
const metrics_1 = require("../../utils/metrics");
|
||||
const docs_files_service_1 = require("./docs-files.service");
|
||||
const docs_collab_service_1 = require("./docs-collab.service");
|
||||
const mkdocs_config_service_1 = require("./mkdocs-config.service");
|
||||
const header_builder_service_1 = require("./header-builder.service");
|
||||
const header_builder_schemas_1 = require("./header-builder.schemas");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_middleware_1.authenticate);
|
||||
router.use(rbac_middleware_1.requireNonTemp);
|
||||
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
|
||||
// GET /api/docs/status — check MkDocs and Code Server availability (content editors only)
|
||||
router.get('/status', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
||||
(0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL),
|
||||
(0, health_check_1.isServiceOnline)(env_1.env.CODE_SERVER_URL),
|
||||
(0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_SITE_SERVER_URL),
|
||||
]);
|
||||
res.json({
|
||||
mkdocs: { online: mkdocsOnline, url: env_1.env.MKDOCS_PREVIEW_URL },
|
||||
codeServer: { online: codeServerOnline, url: env_1.env.CODE_SERVER_URL },
|
||||
siteServer: { online: siteServerOnline, url: env_1.env.MKDOCS_SITE_SERVER_URL },
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to check docs status', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/config — return public-facing port numbers for iframe URLs (content editors only)
|
||||
router.get('/config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, _next) => {
|
||||
res.json({
|
||||
codeServerPort: env_1.env.CODE_SERVER_PORT,
|
||||
mkdocsPort: env_1.env.MKDOCS_PORT,
|
||||
mkdocsSitePort: env_1.env.MKDOCS_SITE_SERVER_PORT,
|
||||
});
|
||||
});
|
||||
// --- MkDocs Config Endpoints ---
|
||||
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content (content editors only)
|
||||
router.get('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig();
|
||||
res.json({ content });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to read mkdocs config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// PUT /api/docs/mkdocs-config — validate + write mkdocs.yml (SUPER_ADMIN only)
|
||||
router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
if (typeof content !== 'string') {
|
||||
res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
await mkdocs_config_service_1.mkdocsConfigService.writeConfig(content);
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
if (err.message?.startsWith('Invalid YAML')) {
|
||||
res.status(400).json({ error: { message: err.message, code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Failed to write mkdocs config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/docs/build — trigger mkdocs build in container
|
||||
router.post('/build', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild();
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('MkDocs build failed', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- Header Builder ---
|
||||
// GET /api/docs/header-config — read header nav bar config (content editors only)
|
||||
router.get('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||
try {
|
||||
const config = await header_builder_service_1.headerBuilderService.readConfig();
|
||||
res.json(config);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to read header config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// PUT /api/docs/header-config — save header nav bar config + regenerate template
|
||||
router.put('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const parsed = header_builder_schemas_1.headerConfigSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
error: { message: 'Invalid header config', code: 'VALIDATION_ERROR', details: parsed.error.flatten().fieldErrors },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await header_builder_service_1.headerBuilderService.writeConfig(parsed.data);
|
||||
// Invalidate docs file tree cache so the new main.html shows up
|
||||
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to save header config', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- File Upload ---
|
||||
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||
'.pdf', '.zip',
|
||||
]);
|
||||
const upload = (0, multer_1.default)({
|
||||
storage: multer_1.default.diskStorage({}), // temp dir
|
||||
limits: { fileSize: 20 * 1024 * 1024 }, // 20MB
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const ext = (0, path_1.extname)(file.originalname).toLowerCase();
|
||||
if (ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||
cb(null, true);
|
||||
}
|
||||
else {
|
||||
cb(new Error(`File type not allowed: ${ext}`));
|
||||
}
|
||||
},
|
||||
});
|
||||
// POST /api/docs/upload — upload binary file (image, pdf, etc.)
|
||||
router.post('/upload', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), upload.single('file'), async (req, res, next) => {
|
||||
const tempPath = req.file?.path;
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'upload' });
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: { message: 'No file provided', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
const targetDir = req.body.path || '';
|
||||
const fileName = (0, path_1.basename)(req.file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const relativePath = targetDir ? `${targetDir}/${fileName}` : fileName;
|
||||
await docs_files_service_1.docsFilesService.uploadFile(relativePath, req.file.path);
|
||||
// Clean up temp file
|
||||
try {
|
||||
await (0, promises_1.rm)(req.file.path);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
res.json({ success: true, path: relativePath });
|
||||
}
|
||||
catch (err) {
|
||||
// Clean up temp file on error
|
||||
if (tempPath) {
|
||||
try {
|
||||
await (0, promises_1.rm)(tempPath);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// --- File Management Endpoints ---
|
||||
// GET /api/docs/files — list file tree (content editors only)
|
||||
router.get('/files', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'list' });
|
||||
if (req.query['force'] === 'true') {
|
||||
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||
}
|
||||
const tree = await docs_files_service_1.docsFilesService.listTree();
|
||||
res.json(tree);
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to list docs files', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/files/search — search files by name/path (content editors only)
|
||||
router.get('/files/search', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
|
||||
if (!search) {
|
||||
res.json({ files: [] });
|
||||
return;
|
||||
}
|
||||
const limit = Math.min(Math.max(Number(req.query['limit']) || 5, 1), 20);
|
||||
const files = await docs_files_service_1.docsFilesService.searchFiles(search, limit);
|
||||
res.json({ files });
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to search docs files', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/docs/files/rename — rename/move file
|
||||
router.post('/files/rename', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'rename' });
|
||||
const { from, to } = req.body;
|
||||
if (!from || !to) {
|
||||
res.status(400).json({ error: { message: 'Both "from" and "to" paths are required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.renameFile(from, to);
|
||||
// Invalidate old path's collaboration state
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(from).catch(() => { });
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// GET /api/docs/files/* — read file content (content editors only)
|
||||
router.get('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'read' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
const content = await docs_files_service_1.docsFilesService.readFileContent(filePath);
|
||||
res.json({ path: filePath, content });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// PUT /api/docs/files/* — write/update file content
|
||||
router.put('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'write' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
const { content } = req.body;
|
||||
if (typeof content !== 'string') {
|
||||
res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.writeFileContent(filePath, content);
|
||||
// Invalidate collaboration state so next session starts fresh from disk
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||
res.json({ success: true, path: filePath });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// POST /api/docs/files/* — create new file or folder
|
||||
router.post('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'create' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
const { content, isDirectory } = req.body;
|
||||
await docs_files_service_1.docsFilesService.createFile(filePath, content, isDirectory);
|
||||
res.status(201).json({ success: true, path: filePath });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
// DELETE /api/docs/files/* — delete file or empty folder
|
||||
router.delete('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||
try {
|
||||
metrics_1.cm_docs_operations.inc({ operation: 'delete' });
|
||||
const filePath = extractWildcardPath(req);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
await docs_files_service_1.docsFilesService.deleteFile(filePath);
|
||||
// Invalidate collaboration state for deleted file
|
||||
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
handleFileError(err, res, next);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Extract the wildcard path from Express 5 req.params.
|
||||
* Express 5 uses params[0] for * routes.
|
||||
*/
|
||||
function extractWildcardPath(req) {
|
||||
// Express 5: req.params is array-like for * routes
|
||||
const params = req.params;
|
||||
const wildcardParam = params[0] || params['0'];
|
||||
if (Array.isArray(wildcardParam))
|
||||
return wildcardParam.join('/');
|
||||
return wildcardParam || '';
|
||||
}
|
||||
function handleFileError(err, res, next) {
|
||||
if (err instanceof docs_files_service_1.PathTraversalError) {
|
||||
res.status(403).json({ error: { message: 'Path traversal not allowed', code: 'FORBIDDEN' } });
|
||||
return;
|
||||
}
|
||||
if (err instanceof docs_files_service_1.FileNotFoundError) {
|
||||
res.status(404).json({ error: { message: err.message, code: 'NOT_FOUND' } });
|
||||
return;
|
||||
}
|
||||
if (err.message?.startsWith('Already exists')) {
|
||||
res.status(409).json({ error: { message: err.message, code: 'CONFLICT' } });
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Directory is not empty') {
|
||||
res.status(400).json({ error: { message: 'Directory is not empty', code: 'VALIDATION_ERROR' } });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Docs file operation failed', err);
|
||||
next(err);
|
||||
}
|
||||
exports.docsRouter = router;
|
||||
//# sourceMappingURL=docs.routes.js.map
|
||||
1
api/dist/modules/docs/docs.routes.js.map
vendored
1
api/dist/modules/docs/docs.routes.js.map
vendored
File diff suppressed because one or more lines are too long
31
api/dist/modules/docs/mkdocs-config.service.d.ts
vendored
31
api/dist/modules/docs/mkdocs-config.service.d.ts
vendored
@@ -1,31 +0,0 @@
|
||||
import { Document } from 'yaml';
|
||||
/**
|
||||
* Parse mkdocs.yml content with support for !!python/name: tags.
|
||||
* Returns a yaml Document that preserves comments and formatting.
|
||||
*/
|
||||
declare function parseConfig(content: string): Document;
|
||||
/**
|
||||
* Validate that a string is valid YAML (with python tags support).
|
||||
* Returns null if valid, error message if invalid.
|
||||
*/
|
||||
declare function validateYaml(content: string): string | null;
|
||||
declare function readConfig(): Promise<string>;
|
||||
declare function writeConfig(content: string): Promise<void>;
|
||||
/**
|
||||
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||
* running inside the MkDocs container on port 8001.
|
||||
*/
|
||||
declare function triggerBuild(): Promise<{
|
||||
success: boolean;
|
||||
output: string;
|
||||
duration: number;
|
||||
}>;
|
||||
export declare const mkdocsConfigService: {
|
||||
readConfig: typeof readConfig;
|
||||
writeConfig: typeof writeConfig;
|
||||
validateYaml: typeof validateYaml;
|
||||
parseConfig: typeof parseConfig;
|
||||
triggerBuild: typeof triggerBuild;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=mkdocs-config.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"mkdocs-config.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,QAAQ,EAAE,MAAM,MAAM,CAAC;AAqB/C;;;GAGG;AACH,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS9C;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpD;AAED,iBAAe,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAED,iBAAe,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezD;AAED;;;GAGG;AACH,iBAAe,YAAY,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7F;AAED,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC"}
|
||||
108
api/dist/modules/docs/mkdocs-config.service.js
vendored
108
api/dist/modules/docs/mkdocs-config.service.js
vendored
@@ -1,108 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.mkdocsConfigService = void 0;
|
||||
const promises_1 = require("fs/promises");
|
||||
const env_1 = require("../../config/env");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const yaml_1 = require("yaml");
|
||||
/**
|
||||
* Custom YAML tag schema to preserve !!python/name: and !!python/object: tags.
|
||||
* Without this, the yaml library would reject these custom tags.
|
||||
*/
|
||||
const pythonNameTag = {
|
||||
identify: () => false,
|
||||
tag: '!python/name',
|
||||
collection: undefined,
|
||||
resolve: (str) => str,
|
||||
};
|
||||
const pythonObjectTag = {
|
||||
identify: () => false,
|
||||
tag: '!python/object',
|
||||
collection: undefined,
|
||||
resolve: (str) => str,
|
||||
};
|
||||
/**
|
||||
* Parse mkdocs.yml content with support for !!python/name: tags.
|
||||
* Returns a yaml Document that preserves comments and formatting.
|
||||
*/
|
||||
function parseConfig(content) {
|
||||
return (0, yaml_1.parseDocument)(content, {
|
||||
customTags: (tags) => [
|
||||
...tags,
|
||||
pythonNameTag,
|
||||
pythonObjectTag,
|
||||
],
|
||||
keepSourceTokens: true,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validate that a string is valid YAML (with python tags support).
|
||||
* Returns null if valid, error message if invalid.
|
||||
*/
|
||||
function validateYaml(content) {
|
||||
try {
|
||||
const doc = parseConfig(content);
|
||||
const errors = doc.errors;
|
||||
if (errors.length > 0) {
|
||||
return errors.map(e => e.message).join('; ');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (err) {
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
async function readConfig() {
|
||||
return (0, promises_1.readFile)(env_1.env.MKDOCS_CONFIG_PATH, 'utf-8');
|
||||
}
|
||||
async function writeConfig(content) {
|
||||
// Validate YAML first
|
||||
const error = validateYaml(content);
|
||||
if (error) {
|
||||
throw new Error(`Invalid YAML: ${error}`);
|
||||
}
|
||||
// Create backup
|
||||
try {
|
||||
await (0, promises_1.copyFile)(env_1.env.MKDOCS_CONFIG_PATH, `${env_1.env.MKDOCS_CONFIG_PATH}.bak`);
|
||||
}
|
||||
catch {
|
||||
logger_1.logger.warn('Could not create backup of mkdocs.yml');
|
||||
}
|
||||
await (0, promises_1.writeFile)(env_1.env.MKDOCS_CONFIG_PATH, content, 'utf-8');
|
||||
}
|
||||
/**
|
||||
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||
* running inside the MkDocs container on port 8001.
|
||||
*/
|
||||
async function triggerBuild() {
|
||||
const buildUrl = `${env_1.env.MKDOCS_PREVIEW_URL.replace(':8000', ':8001')}/build`;
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 130_000);
|
||||
const response = await fetch(buildUrl, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
catch (err) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger_1.logger.error('MkDocs build failed', err);
|
||||
return {
|
||||
success: false,
|
||||
output: `Build error: ${err.message}`,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.mkdocsConfigService = {
|
||||
readConfig,
|
||||
writeConfig,
|
||||
validateYaml,
|
||||
parseConfig,
|
||||
triggerBuild,
|
||||
};
|
||||
//# sourceMappingURL=mkdocs-config.service.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"mkdocs-config.service.js","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,3 +0,0 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=email-templates-admin.routes.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"email-templates-admin.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates-admin.routes.ts"],"names":[],"mappings":"AAuBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqUxB,eAAe,MAAM,CAAC"}
|
||||
@@ -1,272 +0,0 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const email_templates_service_1 = require("./email-templates.service");
|
||||
const email_service_1 = require("../../services/email.service");
|
||||
const validate_1 = require("../../middleware/validate");
|
||||
const email_templates_schemas_1 = require("./email-templates.schemas");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||
const client_1 = require("@prisma/client");
|
||||
const roles_1 = require("../../utils/roles");
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
||||
const redis_1 = require("../../config/redis");
|
||||
const seed_email_templates_1 = require("../../scripts/seed-email-templates");
|
||||
const router = (0, express_1.Router)();
|
||||
// All email template routes require authentication
|
||||
router.use(auth_middleware_1.authenticate);
|
||||
// All routes require broadcast admin role
|
||||
const requireBroadcastRole = (0, rbac_middleware_1.requireRole)(...roles_1.BROADCAST_ROLES);
|
||||
/**
|
||||
* List email templates
|
||||
* GET /email-templates
|
||||
*/
|
||||
router.get('/', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.listEmailTemplatesSchema, 'query'), async (req, res) => {
|
||||
try {
|
||||
const result = await email_templates_service_1.emailTemplatesService.list(req.query);
|
||||
res.json(result);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error listing email templates:', error);
|
||||
res.status(500).json({ error: 'Failed to list email templates' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Get single email template
|
||||
* GET /email-templates/:id
|
||||
*/
|
||||
router.get('/:id', requireBroadcastRole, async (req, res) => {
|
||||
try {
|
||||
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
||||
res.json(template);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'Template not found') {
|
||||
res.status(404).json({ error: 'Template not found' });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error getting email template:', error);
|
||||
res.status(500).json({ error: 'Failed to get email template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Create email template
|
||||
* POST /email-templates
|
||||
*/
|
||||
router.post('/', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.createEmailTemplateSchema), async (req, res) => {
|
||||
try {
|
||||
const template = await email_templates_service_1.emailTemplatesService.create(req.body, req.user.id);
|
||||
res.status(201).json(template);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message.includes('already exists')) {
|
||||
res.status(409).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('validation failed')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error creating email template:', error);
|
||||
res.status(500).json({ error: 'Failed to create email template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Update email template
|
||||
* PUT /email-templates/:id
|
||||
*/
|
||||
router.put('/:id', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.updateEmailTemplateSchema), async (req, res) => {
|
||||
try {
|
||||
const template = await email_templates_service_1.emailTemplatesService.update(req.params.id, req.body, req.user.id);
|
||||
// Clear cache so changes take effect immediately
|
||||
email_service_1.emailService.clearDatabaseCache(template.key);
|
||||
logger_1.logger.info(`Cleared template cache for: ${template.key}`);
|
||||
res.json(template);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'Template not found') {
|
||||
res.status(404).json({ error: 'Template not found' });
|
||||
return;
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('validation failed')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error updating email template:', error);
|
||||
res.status(500).json({ error: 'Failed to update email template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Delete email template
|
||||
* DELETE /email-templates/:id
|
||||
*/
|
||||
router.delete('/:id', requireBroadcastRole, async (req, res) => {
|
||||
try {
|
||||
// Fetch template before deleting to get the key
|
||||
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
||||
await email_templates_service_1.emailTemplatesService.delete(req.params.id);
|
||||
// Clear cache for deleted template
|
||||
email_service_1.emailService.clearDatabaseCache(template.key);
|
||||
logger_1.logger.info(`Cleared template cache for deleted template: ${template.key}`);
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'Template not found') {
|
||||
res.status(404).json({ error: 'Template not found' });
|
||||
return;
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('Cannot delete system templates')) {
|
||||
res.status(403).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error deleting email template:', error);
|
||||
res.status(500).json({ error: 'Failed to delete email template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Get version history
|
||||
* GET /email-templates/:id/versions
|
||||
*/
|
||||
router.get('/:id/versions', requireBroadcastRole, async (req, res) => {
|
||||
try {
|
||||
const versions = await email_templates_service_1.emailTemplatesService.getVersions(req.params.id);
|
||||
res.json(versions);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error getting template versions:', error);
|
||||
res.status(500).json({ error: 'Failed to get template versions' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Get specific version
|
||||
* GET /email-templates/:id/versions/:versionNumber
|
||||
*/
|
||||
router.get('/:id/versions/:versionNumber', requireBroadcastRole, async (req, res) => {
|
||||
try {
|
||||
const version = await email_templates_service_1.emailTemplatesService.getVersion(req.params.id, parseInt(req.params.versionNumber, 10));
|
||||
res.json(version);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'Version not found') {
|
||||
res.status(404).json({ error: 'Version not found' });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error getting template version:', error);
|
||||
res.status(500).json({ error: 'Failed to get template version' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Rollback to previous version
|
||||
* POST /email-templates/:id/rollback
|
||||
*/
|
||||
router.post('/:id/rollback', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.rollbackToVersionSchema), async (req, res) => {
|
||||
try {
|
||||
const template = await email_templates_service_1.emailTemplatesService.rollbackToVersion(req.params.id, req.body, req.user.id);
|
||||
res.json(template);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && (error.message === 'Template not found' || error.message === 'Version not found')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error rolling back template:', error);
|
||||
res.status(500).json({ error: 'Failed to rollback template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Validate template syntax
|
||||
* POST /email-templates/validate
|
||||
*/
|
||||
router.post('/validate', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.validateTemplateSchema), async (req, res) => {
|
||||
try {
|
||||
const result = email_templates_service_1.emailTemplatesService.validateTemplate(req.body);
|
||||
res.json(result);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error validating template:', error);
|
||||
res.status(500).json({ error: 'Failed to validate template' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Send test email
|
||||
* POST /email-templates/:id/test
|
||||
* Rate limited to 10 per 15 minutes per user
|
||||
*/
|
||||
router.post('/:id/test', requireBroadcastRole, (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new rate_limit_redis_1.default({
|
||||
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||
prefix: 'rl:email-template-test:',
|
||||
}),
|
||||
keyGenerator: (req) => req.user.id,
|
||||
}), (0, validate_1.validate)(email_templates_schemas_1.sendTestEmailSchema), async (req, res) => {
|
||||
try {
|
||||
const result = await email_templates_service_1.emailTemplatesService.sendTestEmail(req.params.id, req.body, req.user.id);
|
||||
res.json(result);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'Template not found') {
|
||||
res.status(404).json({ error: 'Template not found' });
|
||||
return;
|
||||
}
|
||||
logger_1.logger.error('Error sending test email:', error);
|
||||
res.status(500).json({ error: 'Failed to send test email' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Get test logs for template
|
||||
* GET /email-templates/:id/test-logs
|
||||
*/
|
||||
router.get('/:id/test-logs', requireBroadcastRole, async (req, res) => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10;
|
||||
const logs = await email_templates_service_1.emailTemplatesService.getTestLogs(req.params.id, limit);
|
||||
res.json(logs);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error getting test logs:', error);
|
||||
res.status(500).json({ error: 'Failed to get test logs' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Seed templates from filesystem (SUPER_ADMIN only)
|
||||
* POST /email-templates/seed
|
||||
*/
|
||||
router.post('/seed', (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN), async (req, res) => {
|
||||
try {
|
||||
await (0, seed_email_templates_1.seedEmailTemplates)();
|
||||
logger_1.logger.info('Email templates seeded via API');
|
||||
res.json({ success: true, message: 'Templates seeded successfully' });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error seeding templates:', error);
|
||||
res.status(500).json({ error: 'Failed to seed templates' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Clear template cache (SUPER_ADMIN only)
|
||||
* POST /email-templates/clear-cache
|
||||
* Body: { key?: string } - Optional template key to clear. If not provided, clears all.
|
||||
*/
|
||||
router.post('/clear-cache', (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN), async (req, res) => {
|
||||
try {
|
||||
const { key } = req.body;
|
||||
email_service_1.emailService.clearDatabaseCache(key);
|
||||
logger_1.logger.info(`Template cache cleared${key ? ` for: ${key}` : ' (all)'}`);
|
||||
res.json({ success: true, cleared: key || 'all' });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Error clearing template cache:', error);
|
||||
res.status(500).json({ error: 'Failed to clear template cache' });
|
||||
}
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=email-templates-admin.routes.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,327 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
export declare const EmailTemplateVariableTypeSchema: z.ZodEnum<["TEXT", "VIDEO"]>;
|
||||
export type EmailTemplateVariableTypeType = z.infer<typeof EmailTemplateVariableTypeSchema>;
|
||||
export declare const emailTemplateVariableSchema: z.ZodEffects<z.ZodObject<{
|
||||
key: z.ZodString;
|
||||
label: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodDefault<z.ZodEnum<["TEXT", "VIDEO"]>>;
|
||||
videoId: z.ZodOptional<z.ZodNumber>;
|
||||
isRequired: z.ZodDefault<z.ZodBoolean>;
|
||||
isConditional: z.ZodDefault<z.ZodBoolean>;
|
||||
sampleValue: z.ZodOptional<z.ZodString>;
|
||||
sortOrder: z.ZodDefault<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>;
|
||||
export declare const listEmailTemplatesSchema: z.ZodObject<{
|
||||
page: z.ZodDefault<z.ZodNumber>;
|
||||
limit: z.ZodDefault<z.ZodNumber>;
|
||||
search: z.ZodOptional<z.ZodString>;
|
||||
category: z.ZodOptional<z.ZodNativeEnum<{
|
||||
INFLUENCE: "INFLUENCE";
|
||||
MAP: "MAP";
|
||||
SYSTEM: "SYSTEM";
|
||||
PAYMENT: "PAYMENT";
|
||||
}>>;
|
||||
isActive: z.ZodOptional<z.ZodBoolean>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
limit: number;
|
||||
page: number;
|
||||
search?: string | undefined;
|
||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
}, {
|
||||
search?: string | undefined;
|
||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||
limit?: number | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
page?: number | undefined;
|
||||
}>;
|
||||
export type ListEmailTemplatesDto = z.infer<typeof listEmailTemplatesSchema>;
|
||||
export declare const createEmailTemplateSchema: z.ZodObject<{
|
||||
key: z.ZodString;
|
||||
name: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
category: z.ZodNativeEnum<{
|
||||
INFLUENCE: "INFLUENCE";
|
||||
MAP: "MAP";
|
||||
SYSTEM: "SYSTEM";
|
||||
PAYMENT: "PAYMENT";
|
||||
}>;
|
||||
subjectLine: z.ZodString;
|
||||
htmlContent: z.ZodString;
|
||||
textContent: z.ZodString;
|
||||
isActive: z.ZodDefault<z.ZodBoolean>;
|
||||
variables: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
|
||||
key: z.ZodString;
|
||||
label: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodDefault<z.ZodEnum<["TEXT", "VIDEO"]>>;
|
||||
videoId: z.ZodOptional<z.ZodNumber>;
|
||||
isRequired: z.ZodDefault<z.ZodBoolean>;
|
||||
isConditional: z.ZodDefault<z.ZodBoolean>;
|
||||
sampleValue: z.ZodOptional<z.ZodString>;
|
||||
sortOrder: z.ZodDefault<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name: string;
|
||||
category: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT";
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
description?: string | undefined;
|
||||
variables?: {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
category: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT";
|
||||
key: string;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isActive?: boolean | undefined;
|
||||
description?: string | undefined;
|
||||
variables?: {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}[] | undefined;
|
||||
}>;
|
||||
export type CreateEmailTemplateDto = z.infer<typeof createEmailTemplateSchema>;
|
||||
export declare const updateEmailTemplateSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
category: z.ZodOptional<z.ZodNativeEnum<{
|
||||
INFLUENCE: "INFLUENCE";
|
||||
MAP: "MAP";
|
||||
SYSTEM: "SYSTEM";
|
||||
PAYMENT: "PAYMENT";
|
||||
}>>;
|
||||
subjectLine: z.ZodOptional<z.ZodString>;
|
||||
htmlContent: z.ZodOptional<z.ZodString>;
|
||||
textContent: z.ZodOptional<z.ZodString>;
|
||||
isActive: z.ZodOptional<z.ZodBoolean>;
|
||||
variables: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
|
||||
key: z.ZodString;
|
||||
label: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodDefault<z.ZodEnum<["TEXT", "VIDEO"]>>;
|
||||
videoId: z.ZodOptional<z.ZodNumber>;
|
||||
isRequired: z.ZodDefault<z.ZodBoolean>;
|
||||
isConditional: z.ZodDefault<z.ZodBoolean>;
|
||||
sampleValue: z.ZodOptional<z.ZodString>;
|
||||
sortOrder: z.ZodDefault<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>, {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}, {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name?: string | undefined;
|
||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
description?: string | null | undefined;
|
||||
subjectLine?: string | undefined;
|
||||
htmlContent?: string | undefined;
|
||||
textContent?: string | undefined;
|
||||
variables?: {
|
||||
type: "VIDEO" | "TEXT";
|
||||
key: string;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
name?: string | undefined;
|
||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
description?: string | null | undefined;
|
||||
subjectLine?: string | undefined;
|
||||
htmlContent?: string | undefined;
|
||||
textContent?: string | undefined;
|
||||
variables?: {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "VIDEO" | "TEXT" | undefined;
|
||||
videoId?: number | undefined;
|
||||
description?: string | undefined;
|
||||
sortOrder?: number | undefined;
|
||||
isRequired?: boolean | undefined;
|
||||
isConditional?: boolean | undefined;
|
||||
sampleValue?: string | undefined;
|
||||
}[] | undefined;
|
||||
}>;
|
||||
export type UpdateEmailTemplateDto = z.infer<typeof updateEmailTemplateSchema>;
|
||||
export declare const rollbackToVersionSchema: z.ZodObject<{
|
||||
versionNumber: z.ZodNumber;
|
||||
changeNotes: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
versionNumber: number;
|
||||
changeNotes?: string | undefined;
|
||||
}, {
|
||||
versionNumber: number;
|
||||
changeNotes?: string | undefined;
|
||||
}>;
|
||||
export type RollbackToVersionDto = z.infer<typeof rollbackToVersionSchema>;
|
||||
export declare const validateTemplateSchema: z.ZodObject<{
|
||||
htmlContent: z.ZodString;
|
||||
textContent: z.ZodString;
|
||||
subjectLine: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
subjectLine?: string | undefined;
|
||||
}, {
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
subjectLine?: string | undefined;
|
||||
}>;
|
||||
export type ValidateTemplateDto = z.infer<typeof validateTemplateSchema>;
|
||||
export declare const sendTestEmailSchema: z.ZodObject<{
|
||||
recipientEmail: z.ZodString;
|
||||
testData: z.ZodRecord<z.ZodString, z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
recipientEmail: string;
|
||||
testData: Record<string, string>;
|
||||
}, {
|
||||
recipientEmail: string;
|
||||
testData: Record<string, string>;
|
||||
}>;
|
||||
export type SendTestEmailDto = z.infer<typeof sendTestEmailSchema>;
|
||||
//# sourceMappingURL=email-templates.schemas.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"email-templates.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,+BAA+B,8BAA4B,CAAC;AACzE,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAG5F,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBvC,CAAC;AAGF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;EAMnC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAG7E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,uBAAuB;;;;;;;;;EAGlC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAG3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;EAIjC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGzE,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
||||
@@ -1,73 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sendTestEmailSchema = exports.validateTemplateSchema = exports.rollbackToVersionSchema = exports.updateEmailTemplateSchema = exports.createEmailTemplateSchema = exports.listEmailTemplatesSchema = exports.emailTemplateVariableSchema = exports.EmailTemplateVariableTypeSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
// Variable type enum
|
||||
exports.EmailTemplateVariableTypeSchema = zod_1.z.enum(['TEXT', 'VIDEO']);
|
||||
// Variable schema
|
||||
exports.emailTemplateVariableSchema = zod_1.z.object({
|
||||
key: zod_1.z.string().regex(/^[A-Z_]+$/, 'Variable key must be uppercase letters and underscores'),
|
||||
label: zod_1.z.string().min(1).max(100),
|
||||
description: zod_1.z.string().max(500).optional(),
|
||||
type: exports.EmailTemplateVariableTypeSchema.default('TEXT'),
|
||||
videoId: zod_1.z.number().int().positive().optional(),
|
||||
isRequired: zod_1.z.boolean().default(true),
|
||||
isConditional: zod_1.z.boolean().default(false),
|
||||
sampleValue: zod_1.z.string().max(1000).optional(),
|
||||
sortOrder: zod_1.z.number().int().min(0).default(0),
|
||||
}).refine((data) => {
|
||||
// VIDEO type must have videoId
|
||||
if (data.type === 'VIDEO' && !data.videoId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, { message: 'VIDEO variables must have a videoId', path: ['videoId'] });
|
||||
// List templates
|
||||
exports.listEmailTemplatesSchema = zod_1.z.object({
|
||||
page: zod_1.z.coerce.number().int().min(1).default(1),
|
||||
limit: zod_1.z.coerce.number().int().min(1).max(100).default(20),
|
||||
search: zod_1.z.string().max(200).optional(),
|
||||
category: zod_1.z.nativeEnum(client_1.EmailTemplateCategory).optional(),
|
||||
isActive: zod_1.z.coerce.boolean().optional(),
|
||||
});
|
||||
// Create template
|
||||
exports.createEmailTemplateSchema = zod_1.z.object({
|
||||
key: zod_1.z.string().regex(/^[a-z0-9-]+$/, 'Template key must be lowercase alphanumeric with hyphens').min(1).max(100),
|
||||
name: zod_1.z.string().min(1).max(200),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
category: zod_1.z.nativeEnum(client_1.EmailTemplateCategory),
|
||||
subjectLine: zod_1.z.string().min(1).max(500),
|
||||
htmlContent: zod_1.z.string().min(1).max(100000, 'HTML content exceeds 100KB limit'),
|
||||
textContent: zod_1.z.string().min(1).max(50000, 'Text content exceeds 50KB limit'),
|
||||
isActive: zod_1.z.boolean().default(true),
|
||||
variables: zod_1.z.array(exports.emailTemplateVariableSchema).optional(),
|
||||
});
|
||||
// Update template
|
||||
exports.updateEmailTemplateSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(200).optional(),
|
||||
description: zod_1.z.string().max(1000).optional().nullable(),
|
||||
category: zod_1.z.nativeEnum(client_1.EmailTemplateCategory).optional(),
|
||||
subjectLine: zod_1.z.string().min(1).max(500).optional(),
|
||||
htmlContent: zod_1.z.string().min(1).max(100000, 'HTML content exceeds 100KB limit').optional(),
|
||||
textContent: zod_1.z.string().min(1).max(50000, 'Text content exceeds 50KB limit').optional(),
|
||||
isActive: zod_1.z.boolean().optional(),
|
||||
variables: zod_1.z.array(exports.emailTemplateVariableSchema).optional(),
|
||||
});
|
||||
// Rollback to version
|
||||
exports.rollbackToVersionSchema = zod_1.z.object({
|
||||
versionNumber: zod_1.z.number().int().min(1),
|
||||
changeNotes: zod_1.z.string().max(500).optional(),
|
||||
});
|
||||
// Validate template
|
||||
exports.validateTemplateSchema = zod_1.z.object({
|
||||
htmlContent: zod_1.z.string().min(1).max(100000),
|
||||
textContent: zod_1.z.string().min(1).max(50000),
|
||||
subjectLine: zod_1.z.string().min(1).max(500).optional(),
|
||||
});
|
||||
// Send test email
|
||||
exports.sendTestEmailSchema = zod_1.z.object({
|
||||
recipientEmail: zod_1.z.string().email('Invalid email address').max(255),
|
||||
testData: zod_1.z.record(zod_1.z.string(), zod_1.z.string()),
|
||||
});
|
||||
//# sourceMappingURL=email-templates.schemas.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"email-templates.schemas.js","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates.schemas.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,266 +0,0 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { ListEmailTemplatesDto, CreateEmailTemplateDto, UpdateEmailTemplateDto, RollbackToVersionDto, ValidateTemplateDto, SendTestEmailDto } from './email-templates.schemas';
|
||||
interface EmailTemplatesListResponse {
|
||||
templates: any[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings?: string[];
|
||||
extractedVariables?: string[];
|
||||
}
|
||||
export declare class EmailTemplatesService {
|
||||
/**
|
||||
* List email templates with pagination, search, and filters
|
||||
*/
|
||||
list(params: ListEmailTemplatesDto): Promise<EmailTemplatesListResponse>;
|
||||
/**
|
||||
* Get a single email template by ID
|
||||
*/
|
||||
getById(id: string): Promise<{
|
||||
_count: {
|
||||
versions: number;
|
||||
testLogs: number;
|
||||
};
|
||||
createdBy: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
};
|
||||
variables: {
|
||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||
id: string;
|
||||
videoId: number | null;
|
||||
key: string;
|
||||
description: string | null;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
templateId: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
sampleValue: string | null;
|
||||
}[];
|
||||
updatedBy: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
} | null;
|
||||
} & {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isSystem: boolean;
|
||||
createdByUserId: string;
|
||||
updatedByUserId: string | null;
|
||||
}>;
|
||||
/**
|
||||
* Get template by key
|
||||
*/
|
||||
getByKey(key: string): Promise<({
|
||||
variables: {
|
||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||
id: string;
|
||||
videoId: number | null;
|
||||
key: string;
|
||||
description: string | null;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
templateId: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
sampleValue: string | null;
|
||||
}[];
|
||||
} & {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isSystem: boolean;
|
||||
createdByUserId: string;
|
||||
updatedByUserId: string | null;
|
||||
}) | null>;
|
||||
/**
|
||||
* Create a new email template
|
||||
*/
|
||||
create(data: CreateEmailTemplateDto, userId: string): Promise<{
|
||||
variables: {
|
||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||
id: string;
|
||||
videoId: number | null;
|
||||
key: string;
|
||||
description: string | null;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
templateId: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
sampleValue: string | null;
|
||||
}[];
|
||||
} & {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isSystem: boolean;
|
||||
createdByUserId: string;
|
||||
updatedByUserId: string | null;
|
||||
}>;
|
||||
/**
|
||||
* Update an email template
|
||||
*/
|
||||
update(id: string, data: UpdateEmailTemplateDto, userId: string): Promise<{
|
||||
variables: {
|
||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||
id: string;
|
||||
videoId: number | null;
|
||||
key: string;
|
||||
description: string | null;
|
||||
sortOrder: number;
|
||||
label: string;
|
||||
templateId: string;
|
||||
isRequired: boolean;
|
||||
isConditional: boolean;
|
||||
sampleValue: string | null;
|
||||
}[];
|
||||
} & {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isSystem: boolean;
|
||||
createdByUserId: string;
|
||||
updatedByUserId: string | null;
|
||||
}>;
|
||||
/**
|
||||
* Delete an email template
|
||||
*/
|
||||
delete(id: string): Promise<void>;
|
||||
/**
|
||||
* Get version history for a template
|
||||
*/
|
||||
getVersions(templateId: string): Promise<({
|
||||
createdBy: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
};
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
createdByUserId: string;
|
||||
versionNumber: number;
|
||||
changeNotes: string | null;
|
||||
templateId: string;
|
||||
})[]>;
|
||||
/**
|
||||
* Get a specific version
|
||||
*/
|
||||
getVersion(templateId: string, versionNumber: number): Promise<{
|
||||
createdBy: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
};
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
createdByUserId: string;
|
||||
versionNumber: number;
|
||||
changeNotes: string | null;
|
||||
templateId: string;
|
||||
}>;
|
||||
/**
|
||||
* Rollback to a previous version
|
||||
*/
|
||||
rollbackToVersion(templateId: string, data: RollbackToVersionDto, userId: string): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
subjectLine: string;
|
||||
htmlContent: string;
|
||||
textContent: string;
|
||||
isSystem: boolean;
|
||||
createdByUserId: string;
|
||||
updatedByUserId: string | null;
|
||||
}>;
|
||||
/**
|
||||
* Validate template syntax
|
||||
*/
|
||||
validateTemplate(data: ValidateTemplateDto): ValidationResult;
|
||||
/**
|
||||
* Send test email
|
||||
*/
|
||||
sendTestEmail(templateId: string, data: SendTestEmailDto, userId: string): Promise<{
|
||||
success: boolean;
|
||||
messageId: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* Get test logs for a template
|
||||
*/
|
||||
getTestLogs(templateId: string, limit?: number): Promise<({
|
||||
sentBy: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
};
|
||||
} & {
|
||||
id: string;
|
||||
success: boolean;
|
||||
messageId: string | null;
|
||||
recipientEmail: string;
|
||||
sentAt: Date;
|
||||
templateId: string;
|
||||
testData: Prisma.JsonValue;
|
||||
errorMessage: string | null;
|
||||
sentByUserId: string;
|
||||
})[]>;
|
||||
}
|
||||
export declare const emailTemplatesService: EmailTemplatesService;
|
||||
export {};
|
||||
//# sourceMappingURL=email-templates.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"email-templates.service.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,2BAA2B,CAAC;AAMnC,UAAU,0BAA0B;IAClC,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,UAAU,EAAE;QACV,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,UAAU,gBAAgB;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,qBAAa,qBAAqB;IAChC;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAuD9E;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6BxB;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAa1B;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsEzD;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8ErE;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM;IAiBvB;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;IAcpC;;OAEG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;;;;;;;;;;;;;;;;;IAsB1D;;OAEG;IACG,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;IAgDtF;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,mBAAmB,GAAG,gBAAgB;IA2D7D;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;;;;IAmE9E;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,SAAK;;;;;;;;;;;;;;;;;CAcjD;AAED,eAAO,MAAM,qBAAqB,uBAA8B,CAAC"}
|
||||
@@ -1,467 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.emailTemplatesService = exports.EmailTemplatesService = void 0;
|
||||
const client_1 = require("@prisma/client");
|
||||
const logger_1 = require("../../utils/logger");
|
||||
const email_service_1 = require("../../services/email.service");
|
||||
const prisma = new client_1.PrismaClient();
|
||||
class EmailTemplatesService {
|
||||
/**
|
||||
* List email templates with pagination, search, and filters
|
||||
*/
|
||||
async list(params) {
|
||||
const { page, limit, search, category, isActive } = params;
|
||||
const skip = (page - 1) * limit;
|
||||
// Build where clause
|
||||
const where = {
|
||||
...(search && {
|
||||
OR: [
|
||||
{ key: { contains: search, mode: 'insensitive' } },
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
],
|
||||
}),
|
||||
...(category && { category }),
|
||||
...(isActive !== undefined && { isActive }),
|
||||
};
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.emailTemplate.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ isSystem: 'desc' }, { category: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
variables: {
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
versions: true,
|
||||
testLogs: true,
|
||||
},
|
||||
},
|
||||
createdBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
updatedBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.emailTemplate.count({ where }),
|
||||
]);
|
||||
return {
|
||||
templates: data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get a single email template by ID
|
||||
*/
|
||||
async getById(id) {
|
||||
const template = await prisma.emailTemplate.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
variables: {
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
versions: true,
|
||||
testLogs: true,
|
||||
},
|
||||
},
|
||||
createdBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
updatedBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!template) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
return template;
|
||||
}
|
||||
/**
|
||||
* Get template by key
|
||||
*/
|
||||
async getByKey(key) {
|
||||
const template = await prisma.emailTemplate.findUnique({
|
||||
where: { key },
|
||||
include: {
|
||||
variables: {
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
return template;
|
||||
}
|
||||
/**
|
||||
* Create a new email template
|
||||
*/
|
||||
async create(data, userId) {
|
||||
// Check for duplicate key
|
||||
const existing = await prisma.emailTemplate.findUnique({
|
||||
where: { key: data.key },
|
||||
});
|
||||
if (existing) {
|
||||
throw new Error(`Template with key "${data.key}" already exists`);
|
||||
}
|
||||
// Validate template content
|
||||
const validation = this.validateTemplate({
|
||||
htmlContent: data.htmlContent,
|
||||
textContent: data.textContent,
|
||||
subjectLine: data.subjectLine,
|
||||
});
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Template validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
// Create template with variables and initial version in transaction
|
||||
const template = await prisma.$transaction(async (tx) => {
|
||||
const newTemplate = await tx.emailTemplate.create({
|
||||
data: {
|
||||
key: data.key,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
category: data.category,
|
||||
subjectLine: data.subjectLine,
|
||||
htmlContent: data.htmlContent,
|
||||
textContent: data.textContent,
|
||||
isSystem: false, // User-created templates are never system templates
|
||||
isActive: data.isActive ?? true,
|
||||
createdByUserId: userId,
|
||||
variables: data.variables
|
||||
? {
|
||||
create: data.variables,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
variables: true,
|
||||
},
|
||||
});
|
||||
// Create initial version
|
||||
await tx.emailTemplateVersion.create({
|
||||
data: {
|
||||
templateId: newTemplate.id,
|
||||
versionNumber: 1,
|
||||
subjectLine: data.subjectLine,
|
||||
htmlContent: data.htmlContent,
|
||||
textContent: data.textContent,
|
||||
changeNotes: 'Initial version',
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
return newTemplate;
|
||||
});
|
||||
logger_1.logger.info(`Email template created: ${template.key} by user ${userId}`);
|
||||
// Clear email service cache for this template
|
||||
email_service_1.emailService.clearDatabaseCache(template.key);
|
||||
return template;
|
||||
}
|
||||
/**
|
||||
* Update an email template
|
||||
*/
|
||||
async update(id, data, userId) {
|
||||
const existing = await this.getById(id);
|
||||
// Validate template content if provided
|
||||
if (data.htmlContent || data.textContent) {
|
||||
const validation = this.validateTemplate({
|
||||
htmlContent: data.htmlContent ?? existing.htmlContent,
|
||||
textContent: data.textContent ?? existing.textContent,
|
||||
subjectLine: data.subjectLine ?? existing.subjectLine,
|
||||
});
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Template validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
// Update template and create new version if content changed
|
||||
const template = await prisma.$transaction(async (tx) => {
|
||||
// Update template
|
||||
const updated = await tx.emailTemplate.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(data.name && { name: data.name }),
|
||||
...(data.description !== undefined && { description: data.description }),
|
||||
...(data.category && { category: data.category }),
|
||||
...(data.subjectLine && { subjectLine: data.subjectLine }),
|
||||
...(data.htmlContent && { htmlContent: data.htmlContent }),
|
||||
...(data.textContent && { textContent: data.textContent }),
|
||||
...(data.isActive !== undefined && { isActive: data.isActive }),
|
||||
updatedByUserId: userId,
|
||||
// Handle variables update
|
||||
...(data.variables && {
|
||||
variables: {
|
||||
deleteMany: {},
|
||||
create: data.variables,
|
||||
},
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
variables: true,
|
||||
},
|
||||
});
|
||||
// Create new version if content changed
|
||||
if (data.subjectLine || data.htmlContent || data.textContent) {
|
||||
const latestVersion = await tx.emailTemplateVersion.findFirst({
|
||||
where: { templateId: id },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
});
|
||||
const nextVersionNumber = (latestVersion?.versionNumber ?? 0) + 1;
|
||||
await tx.emailTemplateVersion.create({
|
||||
data: {
|
||||
templateId: id,
|
||||
versionNumber: nextVersionNumber,
|
||||
subjectLine: data.subjectLine ?? existing.subjectLine,
|
||||
htmlContent: data.htmlContent ?? existing.htmlContent,
|
||||
textContent: data.textContent ?? existing.textContent,
|
||||
changeNotes: `Updated via admin interface`,
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
logger_1.logger.info(`Created version ${nextVersionNumber} for template ${existing.key}`);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
logger_1.logger.info(`Email template updated: ${existing.key} by user ${userId}`);
|
||||
// Clear email service cache
|
||||
email_service_1.emailService.clearDatabaseCache(existing.key);
|
||||
return template;
|
||||
}
|
||||
/**
|
||||
* Delete an email template
|
||||
*/
|
||||
async delete(id) {
|
||||
const existing = await this.getById(id);
|
||||
if (existing.isSystem) {
|
||||
throw new Error('Cannot delete system templates');
|
||||
}
|
||||
await prisma.emailTemplate.delete({
|
||||
where: { id },
|
||||
});
|
||||
logger_1.logger.info(`Email template deleted: ${existing.key}`);
|
||||
// Clear email service cache
|
||||
email_service_1.emailService.clearDatabaseCache(existing.key);
|
||||
}
|
||||
/**
|
||||
* Get version history for a template
|
||||
*/
|
||||
async getVersions(templateId) {
|
||||
const versions = await prisma.emailTemplateVersion.findMany({
|
||||
where: { templateId },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
include: {
|
||||
createdBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return versions;
|
||||
}
|
||||
/**
|
||||
* Get a specific version
|
||||
*/
|
||||
async getVersion(templateId, versionNumber) {
|
||||
const version = await prisma.emailTemplateVersion.findUnique({
|
||||
where: {
|
||||
templateId_versionNumber: {
|
||||
templateId,
|
||||
versionNumber,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
createdBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!version) {
|
||||
throw new Error('Version not found');
|
||||
}
|
||||
return version;
|
||||
}
|
||||
/**
|
||||
* Rollback to a previous version
|
||||
*/
|
||||
async rollbackToVersion(templateId, data, userId) {
|
||||
const template = await this.getById(templateId);
|
||||
const targetVersion = await this.getVersion(templateId, data.versionNumber);
|
||||
// Create new version with content from target version
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const latestVersion = await tx.emailTemplateVersion.findFirst({
|
||||
where: { templateId },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
});
|
||||
const nextVersionNumber = (latestVersion?.versionNumber ?? 0) + 1;
|
||||
// Update template to target version content
|
||||
const updated = await tx.emailTemplate.update({
|
||||
where: { id: templateId },
|
||||
data: {
|
||||
subjectLine: targetVersion.subjectLine,
|
||||
htmlContent: targetVersion.htmlContent,
|
||||
textContent: targetVersion.textContent,
|
||||
updatedByUserId: userId,
|
||||
},
|
||||
});
|
||||
// Create new version (rollback creates a new version, doesn't revert history)
|
||||
await tx.emailTemplateVersion.create({
|
||||
data: {
|
||||
templateId,
|
||||
versionNumber: nextVersionNumber,
|
||||
subjectLine: targetVersion.subjectLine,
|
||||
htmlContent: targetVersion.htmlContent,
|
||||
textContent: targetVersion.textContent,
|
||||
changeNotes: data.changeNotes ?? `Rolled back to version ${data.versionNumber}`,
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
logger_1.logger.info(`Template ${template.key} rolled back to version ${data.versionNumber} by user ${userId}`);
|
||||
// Clear email service cache
|
||||
email_service_1.emailService.clearDatabaseCache(template.key);
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Validate template syntax
|
||||
*/
|
||||
validateTemplate(data) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const variables = new Set();
|
||||
// Extract variables from content
|
||||
const variableRegex = /\{\{([A-Z_]+)\}\}/g;
|
||||
const conditionalRegex = /\{\{#if\s+([A-Z_]+)\}\}|\{\{\/if\}\}/g;
|
||||
// Check HTML content
|
||||
let match;
|
||||
while ((match = variableRegex.exec(data.htmlContent)) !== null) {
|
||||
variables.add(match[1]);
|
||||
}
|
||||
while ((match = conditionalRegex.exec(data.htmlContent)) !== null) {
|
||||
if (match[1])
|
||||
variables.add(match[1]);
|
||||
}
|
||||
// Check text content
|
||||
while ((match = variableRegex.exec(data.textContent)) !== null) {
|
||||
variables.add(match[1]);
|
||||
}
|
||||
while ((match = conditionalRegex.exec(data.textContent)) !== null) {
|
||||
if (match[1])
|
||||
variables.add(match[1]);
|
||||
}
|
||||
// Check subject line if provided
|
||||
if (data.subjectLine) {
|
||||
while ((match = variableRegex.exec(data.subjectLine)) !== null) {
|
||||
variables.add(match[1]);
|
||||
}
|
||||
}
|
||||
// Check for unmatched conditionals
|
||||
const ifCount = (data.htmlContent.match(/\{\{#if/g) || []).length;
|
||||
const endifCount = (data.htmlContent.match(/\{\{\/if\}\}/g) || []).length;
|
||||
if (ifCount !== endifCount) {
|
||||
errors.push('Unmatched {{#if}} conditional blocks in HTML content');
|
||||
}
|
||||
const ifCountText = (data.textContent.match(/\{\{#if/g) || []).length;
|
||||
const endifCountText = (data.textContent.match(/\{\{\/if\}\}/g) || []).length;
|
||||
if (ifCountText !== endifCountText) {
|
||||
errors.push('Unmatched {{#if}} conditional blocks in text content');
|
||||
}
|
||||
// Warn if no variables found
|
||||
if (variables.size === 0) {
|
||||
warnings.push('No template variables found');
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
extractedVariables: Array.from(variables),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Send test email
|
||||
*/
|
||||
async sendTestEmail(templateId, data, userId) {
|
||||
const template = await this.getById(templateId);
|
||||
try {
|
||||
// Process template with test data
|
||||
let htmlContent = template.htmlContent;
|
||||
let textContent = template.textContent;
|
||||
let subjectLine = template.subjectLine;
|
||||
// Replace variables
|
||||
for (const [key, value] of Object.entries(data.testData)) {
|
||||
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
||||
htmlContent = htmlContent.replace(regex, value);
|
||||
textContent = textContent.replace(regex, value);
|
||||
subjectLine = subjectLine.replace(regex, value);
|
||||
}
|
||||
// Handle conditionals (simple implementation)
|
||||
for (const [key, value] of Object.entries(data.testData)) {
|
||||
const ifRegex = new RegExp(`\\{\\{#if\\s+${key}\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}`, 'g');
|
||||
const shouldShow = value && value !== 'false' && value !== '0';
|
||||
htmlContent = htmlContent.replace(ifRegex, shouldShow ? '$1' : '');
|
||||
textContent = textContent.replace(ifRegex, shouldShow ? '$1' : '');
|
||||
}
|
||||
// Send email via email service
|
||||
const result = await email_service_1.emailService.sendEmail({
|
||||
to: data.recipientEmail,
|
||||
subject: subjectLine,
|
||||
text: textContent,
|
||||
html: htmlContent,
|
||||
});
|
||||
// Log test email
|
||||
await prisma.emailTemplateTestLog.create({
|
||||
data: {
|
||||
templateId,
|
||||
recipientEmail: data.recipientEmail,
|
||||
testData: data.testData,
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
sentByUserId: userId,
|
||||
},
|
||||
});
|
||||
logger_1.logger.info(`Test email sent for template ${template.key} to ${data.recipientEmail}`);
|
||||
return { success: true, messageId: result.messageId };
|
||||
}
|
||||
catch (error) {
|
||||
// Log failed test
|
||||
await prisma.emailTemplateTestLog.create({
|
||||
data: {
|
||||
templateId,
|
||||
recipientEmail: data.recipientEmail,
|
||||
testData: data.testData,
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
sentByUserId: userId,
|
||||
},
|
||||
});
|
||||
logger_1.logger.error(`Test email failed for template ${template.key}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get test logs for a template
|
||||
*/
|
||||
async getTestLogs(templateId, limit = 10) {
|
||||
const logs = await prisma.emailTemplateTestLog.findMany({
|
||||
where: { templateId },
|
||||
orderBy: { sentAt: 'desc' },
|
||||
take: limit,
|
||||
include: {
|
||||
sentBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return logs;
|
||||
}
|
||||
}
|
||||
exports.EmailTemplatesService = EmailTemplatesService;
|
||||
exports.emailTemplatesService = new EmailTemplatesService();
|
||||
//# sourceMappingURL=email-templates.service.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
||||
declare const publicRouter: import("express-serve-static-core").Router;
|
||||
declare const adminRouter: import("express-serve-static-core").Router;
|
||||
export { publicRouter as campaignEmailsPublicRouter, adminRouter as campaignEmailsAdminRouter };
|
||||
//# sourceMappingURL=campaign-emails.routes.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaign-emails.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"AAcA,QAAA,MAAM,YAAY,4CAAW,CAAC;AAqC9B,QAAA,MAAM,WAAW,4CAAW,CAAC;AAiC7B,OAAO,EAAE,YAAY,IAAI,0BAA0B,EAAE,WAAW,IAAI,yBAAyB,EAAE,CAAC"}
|
||||
@@ -1,66 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.campaignEmailsAdminRouter = exports.campaignEmailsPublicRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const campaign_emails_service_1 = require("./campaign-emails.service");
|
||||
const campaign_emails_schemas_1 = require("./campaign-emails.schemas");
|
||||
const validate_1 = require("../../../middleware/validate");
|
||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||
const rate_limit_1 = require("../../../middleware/rate-limit");
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
// --- Public Routes (no auth) ---
|
||||
const publicRouter = (0, express_1.Router)();
|
||||
exports.campaignEmailsPublicRouter = publicRouter;
|
||||
// POST /api/campaigns/:slug/send-email
|
||||
publicRouter.post('/:slug/send-email', rate_limit_1.emailRateLimit, (0, validate_1.validate)(campaign_emails_schemas_1.sendCampaignEmailSchema), async (req, res, next) => {
|
||||
try {
|
||||
const slug = req.params.slug;
|
||||
const senderIp = req.ip || req.socket.remoteAddress;
|
||||
const result = await campaign_emails_service_1.campaignEmailsService.sendEmail(slug, req.body, senderIp);
|
||||
res.status(201).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/campaigns/:slug/track-mailto
|
||||
publicRouter.post('/:slug/track-mailto', rate_limit_1.emailRateLimit, (0, validate_1.validate)(campaign_emails_schemas_1.trackMailtoSchema), async (req, res, next) => {
|
||||
try {
|
||||
const slug = req.params.slug;
|
||||
const senderIp = req.ip || req.socket.remoteAddress;
|
||||
const result = await campaign_emails_service_1.campaignEmailsService.trackMailto(slug, req.body, senderIp);
|
||||
res.status(201).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- Admin Routes (auth required) ---
|
||||
const adminRouter = (0, express_1.Router)();
|
||||
exports.campaignEmailsAdminRouter = adminRouter;
|
||||
adminRouter.use(auth_middleware_1.authenticate);
|
||||
adminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||
// GET /api/campaigns/:id/emails
|
||||
adminRouter.get('/:id/emails', (0, validate_1.validate)(campaign_emails_schemas_1.listCampaignEmailsSchema, 'query'), async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const result = await campaign_emails_service_1.campaignEmailsService.listByCampaign(id, req.query);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/campaigns/:id/email-stats
|
||||
adminRouter.get('/:id/email-stats', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const stats = await campaign_emails_service_1.campaignEmailsService.getStats(id);
|
||||
res.json(stats);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=campaign-emails.routes.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaign-emails.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
export declare const sendCampaignEmailSchema: z.ZodObject<{
|
||||
userEmail: z.ZodString;
|
||||
userName: z.ZodString;
|
||||
postalCode: z.ZodString;
|
||||
recipientEmail: z.ZodString;
|
||||
recipientName: z.ZodOptional<z.ZodString>;
|
||||
recipientTitle: z.ZodOptional<z.ZodString>;
|
||||
recipientLevel: z.ZodOptional<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>>;
|
||||
emailMethod: z.ZodNativeEnum<{
|
||||
SMTP: "SMTP";
|
||||
MAILTO: "MAILTO";
|
||||
}>;
|
||||
customEmailSubject: z.ZodOptional<z.ZodString>;
|
||||
customEmailBody: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
recipientEmail: string;
|
||||
emailMethod: "SMTP" | "MAILTO";
|
||||
postalCode: string;
|
||||
recipientName?: string | undefined;
|
||||
recipientTitle?: string | undefined;
|
||||
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
|
||||
customEmailSubject?: string | undefined;
|
||||
customEmailBody?: string | undefined;
|
||||
}, {
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
recipientEmail: string;
|
||||
emailMethod: "SMTP" | "MAILTO";
|
||||
postalCode: string;
|
||||
recipientName?: string | undefined;
|
||||
recipientTitle?: string | undefined;
|
||||
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
|
||||
customEmailSubject?: string | undefined;
|
||||
customEmailBody?: string | undefined;
|
||||
}>;
|
||||
export declare const trackMailtoSchema: z.ZodObject<{
|
||||
recipientEmail: z.ZodString;
|
||||
recipientName: z.ZodOptional<z.ZodString>;
|
||||
recipientTitle: z.ZodOptional<z.ZodString>;
|
||||
recipientLevel: z.ZodOptional<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>>;
|
||||
userEmail: z.ZodOptional<z.ZodString>;
|
||||
userName: z.ZodOptional<z.ZodString>;
|
||||
postalCode: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
recipientEmail: string;
|
||||
userEmail?: string | undefined;
|
||||
userName?: string | undefined;
|
||||
recipientName?: string | undefined;
|
||||
recipientTitle?: string | undefined;
|
||||
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
|
||||
postalCode?: string | undefined;
|
||||
}, {
|
||||
recipientEmail: string;
|
||||
userEmail?: string | undefined;
|
||||
userName?: string | undefined;
|
||||
recipientName?: string | undefined;
|
||||
recipientTitle?: string | undefined;
|
||||
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
|
||||
postalCode?: string | undefined;
|
||||
}>;
|
||||
export declare const listCampaignEmailsSchema: z.ZodObject<{
|
||||
page: z.ZodDefault<z.ZodNumber>;
|
||||
limit: z.ZodDefault<z.ZodNumber>;
|
||||
status: z.ZodOptional<z.ZodNativeEnum<{
|
||||
QUEUED: "QUEUED";
|
||||
SENT: "SENT";
|
||||
FAILED: "FAILED";
|
||||
CLICKED: "CLICKED";
|
||||
USER_INFO_CAPTURED: "USER_INFO_CAPTURED";
|
||||
}>>;
|
||||
emailMethod: z.ZodOptional<z.ZodNativeEnum<{
|
||||
SMTP: "SMTP";
|
||||
MAILTO: "MAILTO";
|
||||
}>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
limit: number;
|
||||
page: number;
|
||||
status?: "QUEUED" | "SENT" | "FAILED" | "CLICKED" | "USER_INFO_CAPTURED" | undefined;
|
||||
emailMethod?: "SMTP" | "MAILTO" | undefined;
|
||||
}, {
|
||||
status?: "QUEUED" | "SENT" | "FAILED" | "CLICKED" | "USER_INFO_CAPTURED" | undefined;
|
||||
limit?: number | undefined;
|
||||
page?: number | undefined;
|
||||
emailMethod?: "SMTP" | "MAILTO" | undefined;
|
||||
}>;
|
||||
export declare const campaignSlugParamSchema: z.ZodObject<{
|
||||
slug: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
slug: string;
|
||||
}, {
|
||||
slug: string;
|
||||
}>;
|
||||
export declare const campaignIdParamSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
}, {
|
||||
id: string;
|
||||
}>;
|
||||
export type SendCampaignEmailInput = z.infer<typeof sendCampaignEmailSchema>;
|
||||
export type TrackMailtoInput = z.infer<typeof trackMailtoSchema>;
|
||||
export type ListCampaignEmailsInput = z.infer<typeof listCampaignEmailsSchema>;
|
||||
//# sourceMappingURL=campaign-emails.schemas.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaign-emails.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWlC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQ5B,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;EAElC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;EAEhC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
||||
@@ -1,39 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.campaignIdParamSchema = exports.campaignSlugParamSchema = exports.listCampaignEmailsSchema = exports.trackMailtoSchema = exports.sendCampaignEmailSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
exports.sendCampaignEmailSchema = zod_1.z.object({
|
||||
userEmail: zod_1.z.string().email('Valid email is required'),
|
||||
userName: zod_1.z.string().min(1, 'Name is required'),
|
||||
postalCode: zod_1.z.string().min(1, 'Postal code is required'),
|
||||
recipientEmail: zod_1.z.string().email('Valid recipient email is required'),
|
||||
recipientName: zod_1.z.string().optional(),
|
||||
recipientTitle: zod_1.z.string().optional(),
|
||||
recipientLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel).optional(),
|
||||
emailMethod: zod_1.z.nativeEnum(client_1.EmailMethod),
|
||||
customEmailSubject: zod_1.z.string().optional(),
|
||||
customEmailBody: zod_1.z.string().optional(),
|
||||
});
|
||||
exports.trackMailtoSchema = zod_1.z.object({
|
||||
recipientEmail: zod_1.z.string().email('Valid recipient email is required'),
|
||||
recipientName: zod_1.z.string().optional(),
|
||||
recipientTitle: zod_1.z.string().optional(),
|
||||
recipientLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel).optional(),
|
||||
userEmail: zod_1.z.string().email().optional(),
|
||||
userName: zod_1.z.string().optional(),
|
||||
postalCode: zod_1.z.string().optional(),
|
||||
});
|
||||
exports.listCampaignEmailsSchema = zod_1.z.object({
|
||||
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
|
||||
status: zod_1.z.nativeEnum(client_1.CampaignEmailStatus).optional(),
|
||||
emailMethod: zod_1.z.nativeEnum(client_1.EmailMethod).optional(),
|
||||
});
|
||||
exports.campaignSlugParamSchema = zod_1.z.object({
|
||||
slug: zod_1.z.string().min(1),
|
||||
});
|
||||
exports.campaignIdParamSchema = zod_1.z.object({
|
||||
id: zod_1.z.string().min(1),
|
||||
});
|
||||
//# sourceMappingURL=campaign-emails.schemas.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaign-emails.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAAmF;AAEtE,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC;IACtD,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAC/C,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,yBAAyB,CAAC;IACxD,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mCAAmC,CAAC;IACrE,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,cAAc,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;IACxD,WAAW,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC;IACtC,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAEU,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mCAAmC,CAAC;IACrE,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,cAAc,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;IACxD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAEU,QAAA,wBAAwB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,4BAAmB,CAAC,CAAC,QAAQ,EAAE;IACpD,WAAW,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAC;AAEU,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACxB,CAAC,CAAC;AAEU,QAAA,qBAAqB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC5C,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtB,CAAC,CAAC"}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { SendCampaignEmailInput, TrackMailtoInput, ListCampaignEmailsInput } from './campaign-emails.schemas';
|
||||
export declare const campaignEmailsService: {
|
||||
sendEmail(slug: string, data: SendCampaignEmailInput, senderIp?: string): Promise<{
|
||||
id: string;
|
||||
status: import(".prisma/client").$Enums.CampaignEmailStatus;
|
||||
emailMethod: import(".prisma/client").$Enums.EmailMethod;
|
||||
}>;
|
||||
trackMailto(slug: string, data: TrackMailtoInput, senderIp?: string): Promise<{
|
||||
id: string;
|
||||
status: import(".prisma/client").$Enums.CampaignEmailStatus;
|
||||
emailMethod: import(".prisma/client").$Enums.EmailMethod;
|
||||
}>;
|
||||
listByCampaign(campaignId: string, filters: ListCampaignEmailsInput): Promise<{
|
||||
emails: {
|
||||
status: import(".prisma/client").$Enums.CampaignEmailStatus;
|
||||
id: string;
|
||||
subject: string;
|
||||
userEmail: string | null;
|
||||
userName: string | null;
|
||||
userPostalCode: string | null;
|
||||
recipientEmail: string;
|
||||
recipientName: string | null;
|
||||
recipientLevel: import(".prisma/client").$Enums.GovernmentLevel | null;
|
||||
emailMethod: import(".prisma/client").$Enums.EmailMethod;
|
||||
sentAt: Date;
|
||||
}[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}>;
|
||||
getStats(campaignId: string): Promise<Record<string, number>>;
|
||||
};
|
||||
//# sourceMappingURL=campaign-emails.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaign-emails.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEnH,eAAO,MAAM,qBAAqB;oBACV,MAAM,QAAQ,sBAAsB,aAAa,MAAM;;;;;sBA+GrD,MAAM,QAAQ,gBAAgB,aAAa,MAAM;;;;;+BAwDxC,MAAM,WAAW,uBAAuB;;;;;;;;;;;;;;;;;;;;;yBA0C9C,MAAM;CAqClC,CAAC"}
|
||||
@@ -1,268 +0,0 @@
|
||||
"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.campaignEmailsService = void 0;
|
||||
const client_1 = require("@prisma/client");
|
||||
const database_1 = require("../../../config/database");
|
||||
const error_handler_1 = require("../../../middleware/error-handler");
|
||||
const email_queue_service_1 = require("../../../services/email-queue.service");
|
||||
const metrics_1 = require("../../../utils/metrics");
|
||||
const crm_activity_1 = require("../../../utils/crm-activity");
|
||||
const group_service_1 = require("../../social/group.service");
|
||||
const achievements_service_1 = require("../../social/achievements.service");
|
||||
exports.campaignEmailsService = {
|
||||
async sendEmail(slug, data, senderIp) {
|
||||
const campaign = await database_1.prisma.campaign.findUnique({
|
||||
where: { slug },
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
status: true,
|
||||
emailSubject: true,
|
||||
emailBody: true,
|
||||
allowSmtpEmail: true,
|
||||
allowMailtoLink: true,
|
||||
allowEmailEditing: true,
|
||||
},
|
||||
});
|
||||
if (!campaign) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
if (campaign.status !== client_1.CampaignStatus.ACTIVE) {
|
||||
throw new error_handler_1.AppError(400, 'Campaign is not active', 'CAMPAIGN_NOT_ACTIVE');
|
||||
}
|
||||
if (data.emailMethod === client_1.EmailMethod.SMTP && !campaign.allowSmtpEmail) {
|
||||
throw new error_handler_1.AppError(400, 'SMTP email is not enabled for this campaign', 'SMTP_NOT_ALLOWED');
|
||||
}
|
||||
if (data.emailMethod === client_1.EmailMethod.MAILTO && !campaign.allowMailtoLink) {
|
||||
throw new error_handler_1.AppError(400, 'Mailto link is not enabled for this campaign', 'MAILTO_NOT_ALLOWED');
|
||||
}
|
||||
// Determine subject and body (custom only if campaign allows editing)
|
||||
const subject = (campaign.allowEmailEditing && data.customEmailSubject)
|
||||
? data.customEmailSubject
|
||||
: campaign.emailSubject;
|
||||
const message = (campaign.allowEmailEditing && data.customEmailBody)
|
||||
? data.customEmailBody
|
||||
: campaign.emailBody;
|
||||
const status = data.emailMethod === client_1.EmailMethod.SMTP
|
||||
? client_1.CampaignEmailStatus.QUEUED
|
||||
: client_1.CampaignEmailStatus.CLICKED;
|
||||
const campaignEmail = await database_1.prisma.campaignEmail.create({
|
||||
data: {
|
||||
campaignId: campaign.id,
|
||||
campaignSlug: campaign.slug,
|
||||
userEmail: data.userEmail,
|
||||
userName: data.userName,
|
||||
userPostalCode: data.postalCode,
|
||||
recipientEmail: data.recipientEmail,
|
||||
recipientName: data.recipientName,
|
||||
recipientTitle: data.recipientTitle,
|
||||
recipientLevel: data.recipientLevel,
|
||||
emailMethod: data.emailMethod,
|
||||
subject,
|
||||
message,
|
||||
status,
|
||||
senderIp,
|
||||
},
|
||||
});
|
||||
if (data.emailMethod === client_1.EmailMethod.SMTP) {
|
||||
await email_queue_service_1.emailQueueService.addCampaignEmail({
|
||||
campaignEmailId: campaignEmail.id,
|
||||
recipientEmail: data.recipientEmail,
|
||||
recipientName: data.recipientName,
|
||||
recipientLevel: data.recipientLevel,
|
||||
userEmail: data.userEmail,
|
||||
userName: data.userName,
|
||||
postalCode: data.postalCode,
|
||||
subject,
|
||||
message,
|
||||
campaignTitle: campaign.title,
|
||||
});
|
||||
}
|
||||
(0, metrics_1.recordCampaignEmail)(campaign.id);
|
||||
// CRM activity (fire-and-forget)
|
||||
(0, crm_activity_1.recordCrmActivity)({
|
||||
email: data.userEmail,
|
||||
activityType: 'EMAIL_SENT',
|
||||
title: `Sent campaign email: ${campaign.title}`,
|
||||
metadata: { campaignId: campaign.id, campaignSlug: campaign.slug, recipientEmail: data.recipientEmail, emailMethod: data.emailMethod },
|
||||
}).catch(() => { });
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncCampaignTeam(campaign.id).catch(() => { });
|
||||
// Achievement check for registered users (fire-and-forget)
|
||||
database_1.prisma.user.findUnique({ where: { email: data.userEmail }, select: { id: true } })
|
||||
.then((user) => {
|
||||
if (user)
|
||||
achievements_service_1.achievementsService.checkAndUnlock(user.id, ['campaigns']).catch(() => { });
|
||||
})
|
||||
.catch(() => { });
|
||||
// Fire-and-forget: check campaign milestones
|
||||
Promise.resolve().then(() => __importStar(require('../../social/impact-stories.service'))).then(({ impactStoriesService }) => {
|
||||
impactStoriesService.checkMilestones(campaign.id).catch(() => { });
|
||||
}).catch(() => { });
|
||||
return {
|
||||
id: campaignEmail.id,
|
||||
status: campaignEmail.status,
|
||||
emailMethod: campaignEmail.emailMethod,
|
||||
};
|
||||
},
|
||||
async trackMailto(slug, data, senderIp) {
|
||||
const campaign = await database_1.prisma.campaign.findUnique({
|
||||
where: { slug },
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
status: true,
|
||||
emailSubject: true,
|
||||
emailBody: true,
|
||||
allowMailtoLink: true,
|
||||
},
|
||||
});
|
||||
if (!campaign) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
if (campaign.status !== client_1.CampaignStatus.ACTIVE) {
|
||||
throw new error_handler_1.AppError(400, 'Campaign is not active', 'CAMPAIGN_NOT_ACTIVE');
|
||||
}
|
||||
const campaignEmail = await database_1.prisma.campaignEmail.create({
|
||||
data: {
|
||||
campaignId: campaign.id,
|
||||
campaignSlug: campaign.slug,
|
||||
userEmail: data.userEmail,
|
||||
userName: data.userName,
|
||||
userPostalCode: data.postalCode,
|
||||
recipientEmail: data.recipientEmail,
|
||||
recipientName: data.recipientName,
|
||||
recipientTitle: data.recipientTitle,
|
||||
recipientLevel: data.recipientLevel,
|
||||
emailMethod: client_1.EmailMethod.MAILTO,
|
||||
subject: campaign.emailSubject,
|
||||
message: campaign.emailBody,
|
||||
status: client_1.CampaignEmailStatus.CLICKED,
|
||||
senderIp,
|
||||
},
|
||||
});
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncCampaignTeam(campaign.id).catch(() => { });
|
||||
// Fire-and-forget: check campaign milestones
|
||||
Promise.resolve().then(() => __importStar(require('../../social/impact-stories.service'))).then(({ impactStoriesService }) => {
|
||||
impactStoriesService.checkMilestones(campaign.id).catch(() => { });
|
||||
}).catch(() => { });
|
||||
return {
|
||||
id: campaignEmail.id,
|
||||
status: campaignEmail.status,
|
||||
emailMethod: campaignEmail.emailMethod,
|
||||
};
|
||||
},
|
||||
async listByCampaign(campaignId, filters) {
|
||||
const { page, limit, status, emailMethod } = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = { campaignId };
|
||||
if (status)
|
||||
where.status = status;
|
||||
if (emailMethod)
|
||||
where.emailMethod = emailMethod;
|
||||
const [emails, total] = await Promise.all([
|
||||
database_1.prisma.campaignEmail.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { sentAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
userEmail: true,
|
||||
userName: true,
|
||||
userPostalCode: true,
|
||||
recipientEmail: true,
|
||||
recipientName: true,
|
||||
recipientLevel: true,
|
||||
emailMethod: true,
|
||||
subject: true,
|
||||
status: true,
|
||||
sentAt: true,
|
||||
},
|
||||
}),
|
||||
database_1.prisma.campaignEmail.count({ where }),
|
||||
]);
|
||||
return {
|
||||
emails,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
async getStats(campaignId) {
|
||||
const [totals, byMethod] = await Promise.all([
|
||||
database_1.prisma.campaignEmail.groupBy({
|
||||
by: ['status'],
|
||||
where: { campaignId },
|
||||
_count: true,
|
||||
}),
|
||||
database_1.prisma.campaignEmail.groupBy({
|
||||
by: ['emailMethod'],
|
||||
where: { campaignId },
|
||||
_count: true,
|
||||
}),
|
||||
]);
|
||||
const stats = {
|
||||
total: 0,
|
||||
queued: 0,
|
||||
sent: 0,
|
||||
failed: 0,
|
||||
clicked: 0,
|
||||
smtpCount: 0,
|
||||
mailtoCount: 0,
|
||||
};
|
||||
for (const row of totals) {
|
||||
stats.total += row._count;
|
||||
const key = row.status.toLowerCase();
|
||||
if (key in stats)
|
||||
stats[key] = row._count;
|
||||
}
|
||||
for (const row of byMethod) {
|
||||
if (row.emailMethod === client_1.EmailMethod.SMTP)
|
||||
stats.smtpCount = row._count;
|
||||
if (row.emailMethod === client_1.EmailMethod.MAILTO)
|
||||
stats.mailtoCount = row._count;
|
||||
}
|
||||
return stats;
|
||||
},
|
||||
};
|
||||
//# sourceMappingURL=campaign-emails.service.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export { router as campaignPublicRouter };
|
||||
//# sourceMappingURL=campaigns-public.routes.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns-public.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2HxB,OAAO,EAAE,MAAM,IAAI,oBAAoB,EAAE,CAAC"}
|
||||
@@ -1,120 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.campaignPublicRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const campaigns_service_1 = require("./campaigns.service");
|
||||
const database_1 = require("../../../config/database");
|
||||
const redis_1 = require("../../../config/redis");
|
||||
const router = (0, express_1.Router)();
|
||||
exports.campaignPublicRouter = router;
|
||||
// GET /api/campaigns/public — list all active campaigns (public)
|
||||
router.get('/public', async (_req, res, next) => {
|
||||
try {
|
||||
const campaigns = await campaigns_service_1.campaignsService.findActiveCampaigns();
|
||||
res.json(campaigns);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/campaigns/:slug/details — public campaign data (ACTIVE only)
|
||||
router.get('/:slug/details', async (req, res, next) => {
|
||||
try {
|
||||
const slug = req.params.slug;
|
||||
const campaign = await campaigns_service_1.campaignsService.findBySlugPublic(slug);
|
||||
res.json(campaign);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/campaigns/:slug/related — related campaigns + upcoming shifts
|
||||
router.get('/:slug/related', async (req, res, next) => {
|
||||
try {
|
||||
const slug = req.params.slug;
|
||||
const cacheKey = `campaign:related:${slug}`;
|
||||
// Check cache
|
||||
try {
|
||||
const cached = await redis_1.redis.get(cacheKey);
|
||||
if (cached) {
|
||||
res.json(JSON.parse(cached));
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { /* cache miss */ }
|
||||
// Find current campaign
|
||||
const campaign = await database_1.prisma.campaign.findFirst({
|
||||
where: { slug, status: 'ACTIVE' },
|
||||
select: { id: true, targetGovernmentLevels: true },
|
||||
});
|
||||
if (!campaign) {
|
||||
res.json({ campaigns: [], shifts: [] });
|
||||
return;
|
||||
}
|
||||
// Related campaigns: same gov levels, exclude current, limit 3
|
||||
const relatedCampaigns = await database_1.prisma.campaign.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
id: { not: campaign.id },
|
||||
...(campaign.targetGovernmentLevels.length > 0 && {
|
||||
targetGovernmentLevels: { hasSome: campaign.targetGovernmentLevels },
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_count: { select: { emails: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 3,
|
||||
});
|
||||
// Related shifts: upcoming, limit 3
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const relatedShifts = await database_1.prisma.shift.findMany({
|
||||
where: {
|
||||
date: { gte: today },
|
||||
status: 'OPEN',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
location: true,
|
||||
maxVolunteers: true,
|
||||
_count: { select: { signups: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
take: 3,
|
||||
});
|
||||
const result = {
|
||||
campaigns: relatedCampaigns.map(c => ({
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
description: c.description?.slice(0, 150) ?? null,
|
||||
emailCount: c._count.emails,
|
||||
})),
|
||||
shifts: relatedShifts.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
startTime: `${s.date.toISOString().split('T')[0]}T${s.startTime}:00`,
|
||||
location: s.location,
|
||||
currentVolunteers: s._count.signups,
|
||||
maxVolunteers: s.maxVolunteers,
|
||||
})),
|
||||
};
|
||||
try {
|
||||
await redis_1.redis.setex(cacheKey, 300, JSON.stringify(result));
|
||||
}
|
||||
catch { /* non-critical */ }
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=campaigns-public.routes.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns-public.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,3 +0,0 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export { router as campaignsRouter };
|
||||
//# sourceMappingURL=campaigns.routes.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6ExB,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,CAAC"}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.campaignsRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const campaigns_service_1 = require("./campaigns.service");
|
||||
const campaigns_schemas_1 = require("./campaigns.schemas");
|
||||
const validate_1 = require("../../../middleware/validate");
|
||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
const router = (0, express_1.Router)();
|
||||
exports.campaignsRouter = router;
|
||||
// All campaign admin routes require authentication + admin role
|
||||
router.use(auth_middleware_1.authenticate);
|
||||
router.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||
// GET /api/campaigns — list campaigns with pagination/filters
|
||||
router.get('/', (0, validate_1.validate)(campaigns_schemas_1.listCampaignsSchema, 'query'), async (req, res, next) => {
|
||||
try {
|
||||
const result = await campaigns_service_1.campaignsService.findAll(req.query, req.user);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/campaigns/:id — get single campaign
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const campaign = await campaigns_service_1.campaignsService.findById(id);
|
||||
res.json(campaign);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/campaigns — create campaign
|
||||
router.post('/', (0, validate_1.validate)(campaigns_schemas_1.createCampaignSchema), async (req, res, next) => {
|
||||
try {
|
||||
const campaign = await campaigns_service_1.campaignsService.create(req.body, req.user);
|
||||
res.status(201).json(campaign);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// PUT /api/campaigns/:id — update campaign
|
||||
router.put('/:id', (0, validate_1.validate)(campaigns_schemas_1.updateCampaignSchema), async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const campaign = await campaigns_service_1.campaignsService.update(id, req.body);
|
||||
res.json(campaign);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// DELETE /api/campaigns/:id — delete campaign
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
await campaigns_service_1.campaignsService.delete(id);
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=campaigns.routes.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,261 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
export declare const createCampaignSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
emailSubject: z.ZodString;
|
||||
emailBody: z.ZodString;
|
||||
callToAction: z.ZodOptional<z.ZodString>;
|
||||
status: z.ZodDefault<z.ZodOptional<z.ZodNativeEnum<{
|
||||
DRAFT: "DRAFT";
|
||||
ACTIVE: "ACTIVE";
|
||||
PAUSED: "PAUSED";
|
||||
ARCHIVED: "ARCHIVED";
|
||||
}>>>;
|
||||
targetGovernmentLevels: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>, "many">>>;
|
||||
allowSmtpEmail: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
allowMailtoLink: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
collectUserInfo: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
showEmailCount: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
showCallCount: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
allowEmailEditing: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
allowCustomRecipients: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
showResponseWall: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
highlightCampaign: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
coverPhoto: z.ZodOptional<z.ZodString>;
|
||||
coverVideoId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
status: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED";
|
||||
title: string;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||
description?: string | undefined;
|
||||
coverPhoto?: string | undefined;
|
||||
coverVideoId?: number | null | undefined;
|
||||
callToAction?: string | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||
description?: string | undefined;
|
||||
coverPhoto?: string | undefined;
|
||||
coverVideoId?: number | null | undefined;
|
||||
callToAction?: string | undefined;
|
||||
allowSmtpEmail?: boolean | undefined;
|
||||
allowMailtoLink?: boolean | undefined;
|
||||
collectUserInfo?: boolean | undefined;
|
||||
showEmailCount?: boolean | undefined;
|
||||
showCallCount?: boolean | undefined;
|
||||
allowEmailEditing?: boolean | undefined;
|
||||
allowCustomRecipients?: boolean | undefined;
|
||||
showResponseWall?: boolean | undefined;
|
||||
highlightCampaign?: boolean | undefined;
|
||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||
}>;
|
||||
export declare const updateCampaignSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
emailSubject: z.ZodOptional<z.ZodString>;
|
||||
emailBody: z.ZodOptional<z.ZodString>;
|
||||
callToAction: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
status: z.ZodOptional<z.ZodNativeEnum<{
|
||||
DRAFT: "DRAFT";
|
||||
ACTIVE: "ACTIVE";
|
||||
PAUSED: "PAUSED";
|
||||
ARCHIVED: "ARCHIVED";
|
||||
}>>;
|
||||
targetGovernmentLevels: z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>, "many">>;
|
||||
allowSmtpEmail: z.ZodOptional<z.ZodBoolean>;
|
||||
allowMailtoLink: z.ZodOptional<z.ZodBoolean>;
|
||||
collectUserInfo: z.ZodOptional<z.ZodBoolean>;
|
||||
showEmailCount: z.ZodOptional<z.ZodBoolean>;
|
||||
showCallCount: z.ZodOptional<z.ZodBoolean>;
|
||||
allowEmailEditing: z.ZodOptional<z.ZodBoolean>;
|
||||
allowCustomRecipients: z.ZodOptional<z.ZodBoolean>;
|
||||
showResponseWall: z.ZodOptional<z.ZodBoolean>;
|
||||
highlightCampaign: z.ZodOptional<z.ZodBoolean>;
|
||||
coverPhoto: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
coverVideoId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
coverPhoto?: string | null | undefined;
|
||||
coverVideoId?: number | null | undefined;
|
||||
emailSubject?: string | undefined;
|
||||
emailBody?: string | undefined;
|
||||
callToAction?: string | null | undefined;
|
||||
allowSmtpEmail?: boolean | undefined;
|
||||
allowMailtoLink?: boolean | undefined;
|
||||
collectUserInfo?: boolean | undefined;
|
||||
showEmailCount?: boolean | undefined;
|
||||
showCallCount?: boolean | undefined;
|
||||
allowEmailEditing?: boolean | undefined;
|
||||
allowCustomRecipients?: boolean | undefined;
|
||||
showResponseWall?: boolean | undefined;
|
||||
highlightCampaign?: boolean | undefined;
|
||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||
}, {
|
||||
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
coverPhoto?: string | null | undefined;
|
||||
coverVideoId?: number | null | undefined;
|
||||
emailSubject?: string | undefined;
|
||||
emailBody?: string | undefined;
|
||||
callToAction?: string | null | undefined;
|
||||
allowSmtpEmail?: boolean | undefined;
|
||||
allowMailtoLink?: boolean | undefined;
|
||||
collectUserInfo?: boolean | undefined;
|
||||
showEmailCount?: boolean | undefined;
|
||||
showCallCount?: boolean | undefined;
|
||||
allowEmailEditing?: boolean | undefined;
|
||||
allowCustomRecipients?: boolean | undefined;
|
||||
showResponseWall?: boolean | undefined;
|
||||
highlightCampaign?: boolean | undefined;
|
||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||
}>;
|
||||
export declare const listCampaignsSchema: z.ZodObject<{
|
||||
page: z.ZodDefault<z.ZodNumber>;
|
||||
limit: z.ZodDefault<z.ZodNumber>;
|
||||
search: z.ZodOptional<z.ZodString>;
|
||||
status: z.ZodOptional<z.ZodNativeEnum<{
|
||||
DRAFT: "DRAFT";
|
||||
ACTIVE: "ACTIVE";
|
||||
PAUSED: "PAUSED";
|
||||
ARCHIVED: "ARCHIVED";
|
||||
}>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
limit: number;
|
||||
page: number;
|
||||
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||
search?: string | undefined;
|
||||
}, {
|
||||
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||
search?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
page?: number | undefined;
|
||||
}>;
|
||||
export declare const campaignIdSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
}, {
|
||||
id: string;
|
||||
}>;
|
||||
export declare const createUserCampaignSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
emailSubject: z.ZodString;
|
||||
emailBody: z.ZodString;
|
||||
callToAction: z.ZodOptional<z.ZodString>;
|
||||
targetGovernmentLevels: z.ZodArray<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>, "many">;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||
description?: string | undefined;
|
||||
callToAction?: string | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||
description?: string | undefined;
|
||||
callToAction?: string | undefined;
|
||||
}>;
|
||||
export declare const updateUserCampaignSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
||||
emailSubject: z.ZodOptional<z.ZodString>;
|
||||
emailBody: z.ZodOptional<z.ZodString>;
|
||||
callToAction: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
||||
targetGovernmentLevels: z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
|
||||
FEDERAL: "FEDERAL";
|
||||
PROVINCIAL: "PROVINCIAL";
|
||||
MUNICIPAL: "MUNICIPAL";
|
||||
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
emailSubject?: string | undefined;
|
||||
emailBody?: string | undefined;
|
||||
callToAction?: string | undefined;
|
||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||
}, {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
emailSubject?: string | undefined;
|
||||
emailBody?: string | undefined;
|
||||
callToAction?: string | undefined;
|
||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||
}>;
|
||||
export declare const moderateCampaignSchema: z.ZodObject<{
|
||||
action: z.ZodEnum<["approve", "reject", "request_changes"]>;
|
||||
reason: z.ZodOptional<z.ZodString>;
|
||||
notes: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
action: "approve" | "reject" | "request_changes";
|
||||
reason?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
}, {
|
||||
action: "approve" | "reject" | "request_changes";
|
||||
reason?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
}>;
|
||||
export declare const listModerationQueueSchema: z.ZodObject<{
|
||||
page: z.ZodDefault<z.ZodNumber>;
|
||||
limit: z.ZodDefault<z.ZodNumber>;
|
||||
search: z.ZodOptional<z.ZodString>;
|
||||
moderationStatus: z.ZodOptional<z.ZodNativeEnum<{
|
||||
PENDING_REVIEW: "PENDING_REVIEW";
|
||||
APPROVED: "APPROVED";
|
||||
REJECTED: "REJECTED";
|
||||
CHANGES_REQUESTED: "CHANGES_REQUESTED";
|
||||
}>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
limit: number;
|
||||
page: number;
|
||||
search?: string | undefined;
|
||||
moderationStatus?: "APPROVED" | "PENDING_REVIEW" | "REJECTED" | "CHANGES_REQUESTED" | undefined;
|
||||
}, {
|
||||
search?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
page?: number | undefined;
|
||||
moderationStatus?: "APPROVED" | "PENDING_REVIEW" | "REJECTED" | "CHANGES_REQUESTED" | undefined;
|
||||
}>;
|
||||
export type CreateCampaignInput = z.infer<typeof createCampaignSchema>;
|
||||
export type UpdateCampaignInput = z.infer<typeof updateCampaignSchema>;
|
||||
export type ListCampaignsInput = z.infer<typeof listCampaignsSchema>;
|
||||
export type CreateUserCampaignInput = z.infer<typeof createUserCampaignSchema>;
|
||||
export type UpdateUserCampaignInput = z.infer<typeof updateUserCampaignSchema>;
|
||||
export type ModerateCampaignInput = z.infer<typeof moderateCampaignSchema>;
|
||||
export type ListModerationQueueInput = z.infer<typeof listModerationQueueSchema>;
|
||||
//# sourceMappingURL=campaigns.schemas.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;EAE3B,CAAC;AAGH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;EAOnC,CAAC;AAGH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;EAAqC,CAAC;AAG3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;EAIjC,CAAC;AAGH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACrE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC/E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC/E,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC3E,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|
||||
@@ -1,79 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.listModerationQueueSchema = exports.moderateCampaignSchema = exports.updateUserCampaignSchema = exports.createUserCampaignSchema = exports.campaignIdSchema = exports.listCampaignsSchema = exports.updateCampaignSchema = exports.createCampaignSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
exports.createCampaignSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1, 'Title is required').max(200),
|
||||
description: zod_1.z.string().max(2000).optional(),
|
||||
emailSubject: zod_1.z.string().min(1, 'Email subject is required').max(200),
|
||||
emailBody: zod_1.z.string().min(1, 'Email body is required').max(10000),
|
||||
callToAction: zod_1.z.string().max(500).optional(),
|
||||
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional().default(client_1.CampaignStatus.DRAFT),
|
||||
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional().default([]),
|
||||
allowSmtpEmail: zod_1.z.boolean().optional().default(true),
|
||||
allowMailtoLink: zod_1.z.boolean().optional().default(true),
|
||||
collectUserInfo: zod_1.z.boolean().optional().default(true),
|
||||
showEmailCount: zod_1.z.boolean().optional().default(true),
|
||||
showCallCount: zod_1.z.boolean().optional().default(true),
|
||||
allowEmailEditing: zod_1.z.boolean().optional().default(false),
|
||||
allowCustomRecipients: zod_1.z.boolean().optional().default(false),
|
||||
showResponseWall: zod_1.z.boolean().optional().default(false),
|
||||
highlightCampaign: zod_1.z.boolean().optional().default(false),
|
||||
coverPhoto: zod_1.z.string().url().max(500).optional(),
|
||||
coverVideoId: zod_1.z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
exports.updateCampaignSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(200).optional(),
|
||||
description: zod_1.z.string().max(2000).nullable().optional(),
|
||||
emailSubject: zod_1.z.string().min(1).max(200).optional(),
|
||||
emailBody: zod_1.z.string().min(1).max(10000).optional(),
|
||||
callToAction: zod_1.z.string().max(500).nullable().optional(),
|
||||
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
|
||||
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional(),
|
||||
allowSmtpEmail: zod_1.z.boolean().optional(),
|
||||
allowMailtoLink: zod_1.z.boolean().optional(),
|
||||
collectUserInfo: zod_1.z.boolean().optional(),
|
||||
showEmailCount: zod_1.z.boolean().optional(),
|
||||
showCallCount: zod_1.z.boolean().optional(),
|
||||
allowEmailEditing: zod_1.z.boolean().optional(),
|
||||
allowCustomRecipients: zod_1.z.boolean().optional(),
|
||||
showResponseWall: zod_1.z.boolean().optional(),
|
||||
highlightCampaign: zod_1.z.boolean().optional(),
|
||||
coverPhoto: zod_1.z.string().url().max(500).nullable().optional(),
|
||||
coverVideoId: zod_1.z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
exports.listCampaignsSchema = zod_1.z.object({
|
||||
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
|
||||
search: zod_1.z.string().optional(),
|
||||
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
|
||||
});
|
||||
exports.campaignIdSchema = zod_1.z.object({
|
||||
id: zod_1.z.string().min(1),
|
||||
});
|
||||
// User-submitted campaign (restricted fields)
|
||||
exports.createUserCampaignSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(3, 'Title must be at least 3 characters').max(200),
|
||||
description: zod_1.z.string().max(2000).optional(),
|
||||
emailSubject: zod_1.z.string().min(3, 'Email subject is required').max(200),
|
||||
emailBody: zod_1.z.string().min(10, 'Email body must be at least 10 characters').max(5000),
|
||||
callToAction: zod_1.z.string().max(500).optional(),
|
||||
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).min(1, 'Select at least one government level'),
|
||||
});
|
||||
// Update own user campaign (same restricted fields)
|
||||
exports.updateUserCampaignSchema = exports.createUserCampaignSchema.partial();
|
||||
// Admin moderation action
|
||||
exports.moderateCampaignSchema = zod_1.z.object({
|
||||
action: zod_1.z.enum(['approve', 'reject', 'request_changes']),
|
||||
reason: zod_1.z.string().max(2000).optional(),
|
||||
notes: zod_1.z.string().max(2000).optional(),
|
||||
});
|
||||
// Moderation queue filters
|
||||
exports.listModerationQueueSchema = zod_1.z.object({
|
||||
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
|
||||
search: zod_1.z.string().optional(),
|
||||
moderationStatus: zod_1.z.nativeEnum(client_1.CampaignModerationStatus).optional(),
|
||||
});
|
||||
//# sourceMappingURL=campaigns.schemas.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,467 +0,0 @@
|
||||
import { UserRole } from '@prisma/client';
|
||||
import type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsInput, CreateUserCampaignInput, ModerateCampaignInput, ListModerationQueueInput } from './campaigns.schemas';
|
||||
interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
}
|
||||
export declare const campaignsService: {
|
||||
findAll(filters: ListCampaignsInput, user?: AuthUser): Promise<{
|
||||
campaigns: {
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}>;
|
||||
findById(id: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
findBySlug(slug: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
create(data: CreateCampaignInput, user: AuthUser): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
update(id: string, data: UpdateCampaignInput): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
findActiveCampaigns(): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
}[]>;
|
||||
findBySlugPublic(slug: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
}>;
|
||||
delete(id: string): Promise<void>;
|
||||
createUserCampaign(data: CreateUserCampaignInput, user: AuthUser): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
findUserCampaigns(userId: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}[]>;
|
||||
updateUserCampaign(id: string, data: Partial<CreateUserCampaignInput>, user: AuthUser): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
findModerationQueue(filters: ListModerationQueueInput): Promise<{
|
||||
campaigns: {
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}>;
|
||||
getModerationStats(): Promise<{
|
||||
total: number;
|
||||
pending: number;
|
||||
approved: number;
|
||||
rejected: number;
|
||||
changesRequested: number;
|
||||
}>;
|
||||
moderateCampaign(id: string, input: ModerateCampaignInput, reviewer: AuthUser): Promise<{
|
||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
responses: number;
|
||||
emails: number;
|
||||
};
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
coverPhoto: string | null;
|
||||
coverVideoId: number | null;
|
||||
moderationNotes: string | null;
|
||||
createdByUserId: string | null;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
callToAction: string | null;
|
||||
allowSmtpEmail: boolean;
|
||||
allowMailtoLink: boolean;
|
||||
collectUserInfo: boolean;
|
||||
showEmailCount: boolean;
|
||||
showCallCount: boolean;
|
||||
allowEmailEditing: boolean;
|
||||
allowCustomRecipients: boolean;
|
||||
showResponseWall: boolean;
|
||||
highlightCampaign: boolean;
|
||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||
createdByUserEmail: string | null;
|
||||
createdByUserName: string | null;
|
||||
isUserGenerated: boolean;
|
||||
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||
reviewedByUserId: string | null;
|
||||
reviewedAt: Date | null;
|
||||
rejectionReason: string | null;
|
||||
}>;
|
||||
};
|
||||
export {};
|
||||
//# sourceMappingURL=campaigns.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"campaigns.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AAI5E,OAAO,KAAK,EACV,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAC5D,uBAAuB,EAA2B,qBAAqB,EAAE,wBAAwB,EAClG,MAAM,qBAAqB,CAAC;AAiH7B,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,eAAO,MAAM,gBAAgB;qBACJ,kBAAkB,SAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CvC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAaF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAaV,mBAAmB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgCrC,MAAM,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA0CrB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAiBlB,MAAM;6BAWQ,uBAAuB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAwCtC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAQT,MAAM,QAAQ,OAAO,CAAC,uBAAuB,CAAC,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCA2CxD,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA0ChC,MAAM,SAAS,qBAAqB,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCpF,CAAC"}
|
||||
@@ -1,408 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.campaignsService = void 0;
|
||||
const client_1 = require("@prisma/client");
|
||||
const database_1 = require("../../../config/database");
|
||||
const error_handler_1 = require("../../../middleware/error-handler");
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
const campaignSelect = {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
emailSubject: true,
|
||||
emailBody: true,
|
||||
callToAction: true,
|
||||
coverPhoto: true,
|
||||
coverVideoId: true,
|
||||
status: true,
|
||||
allowSmtpEmail: true,
|
||||
allowMailtoLink: true,
|
||||
collectUserInfo: true,
|
||||
showEmailCount: true,
|
||||
showCallCount: true,
|
||||
allowEmailEditing: true,
|
||||
allowCustomRecipients: true,
|
||||
showResponseWall: true,
|
||||
highlightCampaign: true,
|
||||
targetGovernmentLevels: true,
|
||||
createdByUserId: true,
|
||||
createdByUserEmail: true,
|
||||
createdByUserName: true,
|
||||
isUserGenerated: true,
|
||||
moderationStatus: true,
|
||||
reviewedByUserId: true,
|
||||
reviewedAt: true,
|
||||
rejectionReason: true,
|
||||
moderationNotes: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
emails: true,
|
||||
responses: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
/** Public-facing select — strips admin-only fields (emails, internal IDs, moderation notes) */
|
||||
const publicCampaignSelect = {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
emailSubject: true,
|
||||
emailBody: true,
|
||||
callToAction: true,
|
||||
coverPhoto: true,
|
||||
coverVideoId: true,
|
||||
status: true,
|
||||
allowSmtpEmail: true,
|
||||
allowMailtoLink: true,
|
||||
collectUserInfo: true,
|
||||
showEmailCount: true,
|
||||
showCallCount: true,
|
||||
allowEmailEditing: true,
|
||||
allowCustomRecipients: true,
|
||||
showResponseWall: true,
|
||||
highlightCampaign: true,
|
||||
targetGovernmentLevels: true,
|
||||
createdByUserName: true,
|
||||
isUserGenerated: true,
|
||||
moderationStatus: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
emails: true,
|
||||
responses: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
}
|
||||
async function resolveSlugCollision(slug, excludeId) {
|
||||
let candidate = slug;
|
||||
let suffix = 2;
|
||||
while (true) {
|
||||
const existing = await database_1.prisma.campaign.findUnique({
|
||||
where: { slug: candidate },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing || (excludeId && existing.id === excludeId)) {
|
||||
return candidate;
|
||||
}
|
||||
candidate = `${slug}-${suffix}`;
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
exports.campaignsService = {
|
||||
async findAll(filters, user) {
|
||||
const { page, limit, search, status } = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = {};
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
if (status)
|
||||
where.status = status;
|
||||
// Non-admin users only see their own campaigns
|
||||
if (user && !(0, roles_1.hasAnyRole)(user, roles_1.ADMIN_ROLES)) {
|
||||
where.createdByUserId = user.id;
|
||||
}
|
||||
const [campaigns, total] = await Promise.all([
|
||||
database_1.prisma.campaign.findMany({
|
||||
where,
|
||||
select: campaignSelect,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
database_1.prisma.campaign.count({ where }),
|
||||
]);
|
||||
return {
|
||||
campaigns,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
async findById(id) {
|
||||
const campaign = await database_1.prisma.campaign.findUnique({
|
||||
where: { id },
|
||||
select: campaignSelect,
|
||||
});
|
||||
if (!campaign) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
return campaign;
|
||||
},
|
||||
async findBySlug(slug) {
|
||||
const campaign = await database_1.prisma.campaign.findUnique({
|
||||
where: { slug },
|
||||
select: campaignSelect,
|
||||
});
|
||||
if (!campaign) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
return campaign;
|
||||
},
|
||||
async create(data, user) {
|
||||
const baseSlug = generateSlug(data.title);
|
||||
const slug = await resolveSlugCollision(baseSlug);
|
||||
// Look up user name from DB
|
||||
const dbUser = await database_1.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { name: true },
|
||||
});
|
||||
// If highlighting this campaign, unset any other highlighted campaign
|
||||
if (data.highlightCampaign) {
|
||||
await database_1.prisma.campaign.updateMany({
|
||||
where: { highlightCampaign: true },
|
||||
data: { highlightCampaign: false },
|
||||
});
|
||||
}
|
||||
const campaign = await database_1.prisma.campaign.create({
|
||||
data: {
|
||||
...data,
|
||||
slug,
|
||||
createdByUserId: user.id,
|
||||
createdByUserEmail: user.email,
|
||||
createdByUserName: dbUser?.name ?? null,
|
||||
},
|
||||
select: campaignSelect,
|
||||
});
|
||||
return campaign;
|
||||
},
|
||||
async update(id, data) {
|
||||
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
const updateData = { ...data };
|
||||
// Regenerate slug if title changes
|
||||
if (data.title && data.title !== existing.title) {
|
||||
const baseSlug = generateSlug(data.title);
|
||||
updateData.slug = await resolveSlugCollision(baseSlug, id);
|
||||
}
|
||||
// If highlighting this campaign, unset any other highlighted campaign
|
||||
if (data.highlightCampaign) {
|
||||
await database_1.prisma.campaign.updateMany({
|
||||
where: { highlightCampaign: true, id: { not: id } },
|
||||
data: { highlightCampaign: false },
|
||||
});
|
||||
}
|
||||
const campaign = await database_1.prisma.campaign.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: campaignSelect,
|
||||
});
|
||||
return campaign;
|
||||
},
|
||||
async findActiveCampaigns() {
|
||||
return database_1.prisma.campaign.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
select: publicCampaignSelect,
|
||||
orderBy: [
|
||||
{ highlightCampaign: 'desc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
});
|
||||
},
|
||||
async findBySlugPublic(slug) {
|
||||
const campaign = await database_1.prisma.campaign.findUnique({
|
||||
where: { slug },
|
||||
select: publicCampaignSelect,
|
||||
});
|
||||
if (!campaign) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
if (campaign.status !== 'ACTIVE') {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
return campaign;
|
||||
},
|
||||
async delete(id) {
|
||||
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
await database_1.prisma.campaign.delete({ where: { id } });
|
||||
},
|
||||
// --- User-Generated Campaign Methods ---
|
||||
async createUserCampaign(data, user) {
|
||||
const baseSlug = generateSlug(data.title);
|
||||
const slug = await resolveSlugCollision(baseSlug);
|
||||
const dbUser = await database_1.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { name: true },
|
||||
});
|
||||
const campaign = await database_1.prisma.campaign.create({
|
||||
data: {
|
||||
slug,
|
||||
title: escapeHtml(data.title),
|
||||
description: data.description ? escapeHtml(data.description) : null,
|
||||
emailSubject: escapeHtml(data.emailSubject),
|
||||
emailBody: escapeHtml(data.emailBody),
|
||||
callToAction: data.callToAction ? escapeHtml(data.callToAction) : null,
|
||||
targetGovernmentLevels: data.targetGovernmentLevels,
|
||||
status: 'DRAFT',
|
||||
isUserGenerated: true,
|
||||
moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW,
|
||||
allowSmtpEmail: false,
|
||||
allowMailtoLink: true,
|
||||
collectUserInfo: true,
|
||||
showEmailCount: true,
|
||||
showCallCount: false,
|
||||
allowEmailEditing: false,
|
||||
allowCustomRecipients: false,
|
||||
showResponseWall: false,
|
||||
highlightCampaign: false,
|
||||
createdByUserId: user.id,
|
||||
createdByUserEmail: user.email,
|
||||
createdByUserName: dbUser?.name ?? null,
|
||||
},
|
||||
select: campaignSelect,
|
||||
});
|
||||
return campaign;
|
||||
},
|
||||
async findUserCampaigns(userId) {
|
||||
return database_1.prisma.campaign.findMany({
|
||||
where: { createdByUserId: userId, isUserGenerated: true },
|
||||
select: campaignSelect,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
async updateUserCampaign(id, data, user) {
|
||||
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
if (existing.createdByUserId !== user.id) {
|
||||
throw new error_handler_1.AppError(403, 'You can only edit your own campaigns', 'FORBIDDEN');
|
||||
}
|
||||
if (!existing.isUserGenerated) {
|
||||
throw new error_handler_1.AppError(403, 'Cannot edit admin-created campaigns', 'FORBIDDEN');
|
||||
}
|
||||
if (existing.moderationStatus !== client_1.CampaignModerationStatus.CHANGES_REQUESTED &&
|
||||
existing.moderationStatus !== client_1.CampaignModerationStatus.PENDING_REVIEW) {
|
||||
throw new error_handler_1.AppError(400, 'Campaign cannot be edited in its current state', 'INVALID_STATE');
|
||||
}
|
||||
const updateData = {};
|
||||
if (data.title) {
|
||||
updateData.title = escapeHtml(data.title);
|
||||
const baseSlug = generateSlug(data.title);
|
||||
updateData.slug = await resolveSlugCollision(baseSlug, id);
|
||||
}
|
||||
if (data.description !== undefined)
|
||||
updateData.description = data.description ? escapeHtml(data.description) : null;
|
||||
if (data.emailSubject)
|
||||
updateData.emailSubject = escapeHtml(data.emailSubject);
|
||||
if (data.emailBody)
|
||||
updateData.emailBody = escapeHtml(data.emailBody);
|
||||
if (data.callToAction !== undefined)
|
||||
updateData.callToAction = data.callToAction ? escapeHtml(data.callToAction) : null;
|
||||
if (data.targetGovernmentLevels)
|
||||
updateData.targetGovernmentLevels = data.targetGovernmentLevels;
|
||||
// Reset to pending review on edit
|
||||
updateData.moderationStatus = client_1.CampaignModerationStatus.PENDING_REVIEW;
|
||||
updateData.rejectionReason = null;
|
||||
return database_1.prisma.campaign.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: campaignSelect,
|
||||
});
|
||||
},
|
||||
// --- Moderation Methods ---
|
||||
async findModerationQueue(filters) {
|
||||
const { page, limit, search, moderationStatus } = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = { isUserGenerated: true };
|
||||
if (moderationStatus)
|
||||
where.moderationStatus = moderationStatus;
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ createdByUserName: { contains: search, mode: 'insensitive' } },
|
||||
{ createdByUserEmail: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
const [campaigns, total] = await Promise.all([
|
||||
database_1.prisma.campaign.findMany({
|
||||
where,
|
||||
select: campaignSelect,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
database_1.prisma.campaign.count({ where }),
|
||||
]);
|
||||
return {
|
||||
campaigns,
|
||||
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
||||
};
|
||||
},
|
||||
async getModerationStats() {
|
||||
const [total, pending, approved, rejected, changesRequested] = await Promise.all([
|
||||
database_1.prisma.campaign.count({ where: { isUserGenerated: true } }),
|
||||
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW } }),
|
||||
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.APPROVED } }),
|
||||
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.REJECTED } }),
|
||||
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.CHANGES_REQUESTED } }),
|
||||
]);
|
||||
return { total, pending, approved, rejected, changesRequested };
|
||||
},
|
||||
async moderateCampaign(id, input, reviewer) {
|
||||
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||
}
|
||||
if (!existing.isUserGenerated) {
|
||||
throw new error_handler_1.AppError(400, 'Only user-generated campaigns can be moderated', 'INVALID_STATE');
|
||||
}
|
||||
const updateData = {
|
||||
reviewedByUserId: reviewer.id,
|
||||
reviewedAt: new Date(),
|
||||
moderationNotes: input.notes ?? null,
|
||||
};
|
||||
switch (input.action) {
|
||||
case 'approve':
|
||||
updateData.moderationStatus = client_1.CampaignModerationStatus.APPROVED;
|
||||
updateData.status = 'ACTIVE';
|
||||
updateData.rejectionReason = null;
|
||||
break;
|
||||
case 'reject':
|
||||
updateData.moderationStatus = client_1.CampaignModerationStatus.REJECTED;
|
||||
updateData.rejectionReason = input.reason ?? null;
|
||||
break;
|
||||
case 'request_changes':
|
||||
updateData.moderationStatus = client_1.CampaignModerationStatus.CHANGES_REQUESTED;
|
||||
updateData.rejectionReason = input.reason ?? null;
|
||||
break;
|
||||
}
|
||||
return database_1.prisma.campaign.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: campaignSelect,
|
||||
});
|
||||
},
|
||||
};
|
||||
//# sourceMappingURL=campaigns.service.js.map
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user