feat: complete V1 phases 0-7
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export interface DatabaseConnectionOptions {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
function requiredEnvironmentValue(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(name: string, fallback: number): number {
|
||||
const raw = process.env[name]?.trim();
|
||||
if (!raw) return fallback;
|
||||
const value = Number(raw);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`${name} must be a positive integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseBoolean(name: string, fallback: boolean): boolean {
|
||||
const raw = process.env[name]?.trim().toLowerCase();
|
||||
if (!raw) return fallback;
|
||||
if (raw === 'true') return true;
|
||||
if (raw === 'false') return false;
|
||||
throw new Error(`${name} must be true or false`);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppConfigService {
|
||||
readonly appEnvironment = process.env.APP_ENV?.trim() || 'development';
|
||||
readonly apiPort = positiveInteger('API_PORT', 3000);
|
||||
readonly databaseUrl = requiredEnvironmentValue('DATABASE_URL');
|
||||
readonly sessionTokenPepper = requiredEnvironmentValue('SESSION_TOKEN_PEPPER');
|
||||
readonly auditIpHashKey = requiredEnvironmentValue('AUDIT_IP_HASH_KEY');
|
||||
readonly adminWebOrigin = requiredEnvironmentValue('ADMIN_WEB_ORIGIN');
|
||||
readonly adminCookieSecure = parseBoolean('ADMIN_COOKIE_SECURE', true);
|
||||
readonly adminSessionTtlSeconds = positiveInteger('AUTH_SESSION_TTL_SECONDS', 28_800);
|
||||
readonly wechatSessionTtlSeconds = positiveInteger('WECHAT_SESSION_TTL_SECONDS', 7_200);
|
||||
readonly wechatAppId = process.env.WECHAT_APP_ID?.trim() || null;
|
||||
readonly wechatAppSecret = process.env.WECHAT_APP_SECRET?.trim() || null;
|
||||
readonly wechatNotificationTemplates = {
|
||||
APPLICATION_SUBMITTED: process.env.WECHAT_TEMPLATE_APPLICATION_SUBMITTED?.trim() || null,
|
||||
APPLICATION_ACCEPTED: process.env.WECHAT_TEMPLATE_APPLICATION_ACCEPTED?.trim() || null,
|
||||
SERVICE_ARRANGED: process.env.WECHAT_TEMPLATE_SERVICE_ARRANGED?.trim() || null,
|
||||
SERVICE_COMPLETED: process.env.WECHAT_TEMPLATE_SERVICE_COMPLETED?.trim() || null,
|
||||
FOLLOW_UP_PENDING: process.env.WECHAT_TEMPLATE_FOLLOW_UP_PENDING?.trim() || null,
|
||||
ACTIVITY_REGISTRATION_RESULT:
|
||||
process.env.WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT?.trim() || null,
|
||||
} as const;
|
||||
readonly notificationWorkerEnabled = parseBoolean('NOTIFICATION_WORKER_ENABLED', false);
|
||||
readonly objectStorageEndpoint = process.env.OBJECT_STORAGE_ENDPOINT?.trim() || null;
|
||||
readonly objectStorageRegion = process.env.OBJECT_STORAGE_REGION?.trim() || 'auto';
|
||||
readonly objectStorageBucket = process.env.OBJECT_STORAGE_BUCKET?.trim() || null;
|
||||
readonly objectStorageAccessKey = process.env.OBJECT_STORAGE_ACCESS_KEY?.trim() || null;
|
||||
readonly objectStorageSecretKey = process.env.OBJECT_STORAGE_SECRET_KEY?.trim() || null;
|
||||
readonly objectStorageForcePathStyle = parseBoolean('OBJECT_STORAGE_FORCE_PATH_STYLE', true);
|
||||
readonly fieldEncryptionKey = this.parseEncryptionKey();
|
||||
readonly database = this.parseDatabaseUrl();
|
||||
|
||||
get isProduction(): boolean {
|
||||
return this.appEnvironment === 'production';
|
||||
}
|
||||
|
||||
get objectStorageConfigured(): boolean {
|
||||
return Boolean(
|
||||
this.objectStorageEndpoint &&
|
||||
this.objectStorageBucket &&
|
||||
this.objectStorageAccessKey &&
|
||||
this.objectStorageSecretKey,
|
||||
);
|
||||
}
|
||||
get wechatNotificationConfigured(): boolean {
|
||||
return Boolean(
|
||||
this.wechatAppId &&
|
||||
this.wechatAppSecret &&
|
||||
Object.values(this.wechatNotificationTemplates).every(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private parseEncryptionKey(): Buffer {
|
||||
const encoded = requiredEnvironmentValue('FIELD_ENCRYPTION_KEY');
|
||||
const key = Buffer.from(encoded, 'base64');
|
||||
if (key.length !== 32) {
|
||||
throw new Error('FIELD_ENCRYPTION_KEY must be a base64-encoded 32-byte key');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private parseDatabaseUrl(): DatabaseConnectionOptions {
|
||||
const url = new URL(this.databaseUrl);
|
||||
if (url.protocol !== 'mysql:') {
|
||||
throw new Error('DATABASE_URL must use the mysql protocol');
|
||||
}
|
||||
const database = url.pathname.replace(/^\//, '');
|
||||
if (!url.hostname || !url.username || !database) {
|
||||
throw new Error('DATABASE_URL must include host, user, and database');
|
||||
}
|
||||
return {
|
||||
host: url.hostname,
|
||||
port: url.port ? Number(url.port) : 3306,
|
||||
user: decodeURIComponent(url.username),
|
||||
password: decodeURIComponent(url.password),
|
||||
database,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user