first commit

This commit is contained in:
2025-11-01 09:24:26 +08:00
commit 6516d8dc78
87 changed files with 7558 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import mongoose from 'mongoose';
import { env } from './env.js';
import { logger } from './logger.js';
export async function connectToDatabase(): Promise<void> {
if (!env.MONGODB_URI) {
logger.warn('Skipping MongoDB connection: MONGODB_URI is not set.');
return;
}
try {
const connectionUri = env.MONGODB_URI;
const options: mongoose.ConnectOptions = {};
if (env.MONGODB_DB) {
options.dbName = env.MONGODB_DB;
}
if (env.MONGODB_USER && env.MONGODB_PASS) {
options.auth = {
username: env.MONGODB_USER,
password: env.MONGODB_PASS
};
options.authMechanism = 'SCRAM-SHA-256';
}
if (env.MONGODB_AUTH_SOURCE) {
options.authSource = env.MONGODB_AUTH_SOURCE;
}
logger.info({ uri: connectionUri, db: options.dbName, authSource: options.authSource }, 'Attempting MongoDB connection');
await mongoose.connect(connectionUri, options);
logger.info('MongoDB connected');
} catch (error) {
logger.error({ error }, 'Failed to connect to MongoDB');
throw error;
}
}
export async function disconnectFromDatabase(): Promise<void> {
if (mongoose.connection.readyState !== 0) {
await mongoose.disconnect();
logger.info('MongoDB disconnected');
}
}

View File

@@ -0,0 +1,45 @@
import { z } from 'zod';
const DEFAULT_NOTIFICATION_PACKAGES = [
'com.eg.android.AlipayGphone',
'com.tencent.mm',
'com.tencent.mobileqq',
'com.google.android.gms',
'com.android.mms'
];
const envSchema = z
.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
HOST: z.string().default('0.0.0.0'),
MONGODB_URI: z.string().url().optional(),
MONGODB_DB: z.string().optional(),
MONGODB_USER: z.string().optional(),
MONGODB_PASS: z.string().optional(),
MONGODB_AUTH_SOURCE: z.string().optional(),
JWT_SECRET: z.string().min(10).default('change-me-in-prod'),
JWT_REFRESH_SECRET: z.string().min(10).optional(),
NOTIFICATION_WEBHOOK_SECRET: z.string().min(10).optional(),
NOTIFICATION_ALLOWED_PACKAGES: z.string().optional(),
GEMINI_API_KEY: z.string().optional(),
OCR_PROJECT_ID: z.string().optional(),
FILE_STORAGE_BUCKET: z.string().optional()
})
.transform((values) => ({
...values,
isProduction: values.NODE_ENV === 'production',
notificationPackageWhitelist: values.NOTIFICATION_ALLOWED_PACKAGES
? values.NOTIFICATION_ALLOWED_PACKAGES.split(',').map((pkg) => pkg.trim()).filter(Boolean)
: DEFAULT_NOTIFICATION_PACKAGES
}));
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
throw new Error('Invalid environment configuration.');
}
export const env = parsed.data;
export type Env = typeof env;

View File

@@ -0,0 +1,17 @@
import pino from 'pino';
const level = process.env.LOG_LEVEL ?? (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
export const logger = pino({
level,
transport:
process.env.NODE_ENV === 'production'
? undefined
: {
target: 'pino-pretty',
options: {
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
});