saas-backend-kit 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +17 -3
- package/CHANGELOG.md +0 -31
- package/PUBLISHING.md +0 -133
- package/copy-dts.js +0 -314
- package/examples/express/.env.example +0 -44
- package/examples/express/app.ts +0 -203
- package/jest-output.json +0 -72
- package/jest.config.js +0 -19
- package/saas-banner.svg +0 -239
- package/src/auth/express.ts +0 -250
- package/src/auth/fastify.ts +0 -65
- package/src/auth/index.ts +0 -6
- package/src/auth/jwt.ts +0 -47
- package/src/auth/oauth.ts +0 -117
- package/src/auth/rbac.ts +0 -82
- package/src/auth/types.ts +0 -69
- package/src/config/index.ts +0 -126
- package/src/database/index.ts +0 -102
- package/src/index.ts +0 -19
- package/src/logger/index.ts +0 -110
- package/src/notifications/index.ts +0 -262
- package/src/plugin.ts +0 -192
- package/src/queue/index.ts +0 -208
- package/src/rate-limit/express.ts +0 -145
- package/src/rate-limit/fastify.ts +0 -47
- package/src/rate-limit/index.ts +0 -2
- package/src/response/index.ts +0 -206
- package/src/upload/index.ts +0 -268
- package/src/utils/index.ts +0 -180
- package/tests/auth.test.ts +0 -134
- package/tests/config.test.ts +0 -36
- package/tests/logger.test.ts +0 -47
- package/tests/notifications.test.ts +0 -19
- package/tests/rate-limit.test.ts +0 -50
- package/tests/upload.test.ts +0 -33
- package/tsconfig.json +0 -30
- package/tsconfig.test.json +0 -14
- package/tsup.config.ts +0 -26
package/src/config/index.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const envSchema = z.object({
|
|
4
|
-
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
5
|
-
PORT: z.string().default('3000'),
|
|
6
|
-
DATABASE_URL: z.string().optional(),
|
|
7
|
-
MONGODB_URL: z.string().optional(),
|
|
8
|
-
REDIS_URL: z.string().default('redis://localhost:6379'),
|
|
9
|
-
JWT_SECRET: z.string().min(32).optional(),
|
|
10
|
-
JWT_EXPIRES_IN: z.string().default('7d'),
|
|
11
|
-
JWT_REFRESH_SECRET: z.string().min(32).optional(),
|
|
12
|
-
JWT_REFRESH_EXPIRES_IN: z.string().default('30d'),
|
|
13
|
-
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
14
|
-
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
15
|
-
GOOGLE_REDIRECT_URI: z.string().optional(),
|
|
16
|
-
SMTP_HOST: z.string().optional(),
|
|
17
|
-
SMTP_PORT: z.string().default('587'),
|
|
18
|
-
SMTP_USER: z.string().optional(),
|
|
19
|
-
SMTP_PASS: z.string().optional(),
|
|
20
|
-
SMTP_FROM: z.string().optional(),
|
|
21
|
-
TWILIO_ACCOUNT_SID: z.string().optional(),
|
|
22
|
-
TWILIO_AUTH_TOKEN: z.string().optional(),
|
|
23
|
-
TWILIO_PHONE_NUMBER: z.string().optional(),
|
|
24
|
-
SLACK_WEBHOOK_URL: z.string().optional(),
|
|
25
|
-
RATE_LIMIT_WINDOW: z.string().default('1m'),
|
|
26
|
-
RATE_LIMIT_LIMIT: z.string().default('100'),
|
|
27
|
-
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
28
|
-
AWS_REGION: z.string().default('us-east-1'),
|
|
29
|
-
AWS_ACCESS_KEY_ID: z.string().optional(),
|
|
30
|
-
AWS_SECRET_ACCESS_KEY: z.string().optional(),
|
|
31
|
-
AWS_S3_BUCKET: z.string().optional(),
|
|
32
|
-
AWS_ENDPOINT: z.string().optional(),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export type EnvConfig = z.infer<typeof envSchema>;
|
|
36
|
-
|
|
37
|
-
export interface ConfigOptions {
|
|
38
|
-
schema?: z.ZodSchema;
|
|
39
|
-
envPath?: string;
|
|
40
|
-
validate?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class ConfigManager {
|
|
44
|
-
private config: EnvConfig | null = null;
|
|
45
|
-
private schema: z.ZodSchema;
|
|
46
|
-
private validate: boolean;
|
|
47
|
-
|
|
48
|
-
constructor(options: ConfigOptions = {}) {
|
|
49
|
-
this.schema = options.schema || envSchema;
|
|
50
|
-
this.validate = options.validate ?? true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
load(): EnvConfig {
|
|
54
|
-
if (this.config) return this.config;
|
|
55
|
-
|
|
56
|
-
const env: Record<string, string | undefined> = {};
|
|
57
|
-
|
|
58
|
-
for (const key of Object.keys(this.schema.shape)) {
|
|
59
|
-
env[key] = process.env[key];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (this.validate) {
|
|
63
|
-
const result = this.schema.safeParse(env);
|
|
64
|
-
if (!result.success) {
|
|
65
|
-
const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
66
|
-
throw new Error(`Config validation failed: ${errors}`);
|
|
67
|
-
}
|
|
68
|
-
this.config = result.data;
|
|
69
|
-
} else {
|
|
70
|
-
this.config = env as EnvConfig;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return this.config;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
get<K extends keyof EnvConfig>(key: K): EnvConfig[K] {
|
|
77
|
-
if (!this.config) this.load();
|
|
78
|
-
return this.config![key];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
int(key: keyof EnvConfig): number {
|
|
82
|
-
const value = this.get(key);
|
|
83
|
-
if (typeof value === 'string') return parseInt(value, 10);
|
|
84
|
-
return Number(value);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
bool(key: keyof EnvConfig): boolean {
|
|
88
|
-
const value = this.get(key);
|
|
89
|
-
if (typeof value === 'boolean') return value;
|
|
90
|
-
if (typeof value === 'string') return value.toLowerCase() === 'true';
|
|
91
|
-
return Boolean(value);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
isProduction(): boolean {
|
|
95
|
-
return this.get('NODE_ENV') === 'production';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
isDevelopment(): boolean {
|
|
99
|
-
return this.get('NODE_ENV') === 'development';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
isTest(): boolean {
|
|
103
|
-
return this.get('NODE_ENV') === 'test';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getAll(): EnvConfig {
|
|
107
|
-
if (!this.config) this.load();
|
|
108
|
-
return this.config!;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const globalConfig = new ConfigManager();
|
|
113
|
-
|
|
114
|
-
export const config = {
|
|
115
|
-
load: () => globalConfig.load(),
|
|
116
|
-
get: <K extends keyof EnvConfig>(key: K) => globalConfig.get(key),
|
|
117
|
-
int: (key: keyof EnvConfig) => globalConfig.int(key),
|
|
118
|
-
bool: (key: keyof EnvConfig) => globalConfig.bool(key),
|
|
119
|
-
isProduction: () => globalConfig.isProduction(),
|
|
120
|
-
isDevelopment: () => globalConfig.isDevelopment(),
|
|
121
|
-
isTest: () => globalConfig.isTest(),
|
|
122
|
-
getAll: () => globalConfig.getAll(),
|
|
123
|
-
create: (options?: ConfigOptions) => new ConfigManager(options),
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export default config;
|
package/src/database/index.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { MongoClient, Db, Collection, MongoClientOptions } from 'mongodb';
|
|
2
|
-
|
|
3
|
-
export interface DatabaseConfig {
|
|
4
|
-
url?: string;
|
|
5
|
-
options?: MongoClientOptions;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DatabaseOptions {
|
|
9
|
-
url: string;
|
|
10
|
-
options?: MongoClientOptions;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class DatabaseManager {
|
|
14
|
-
private client: MongoClient | null = null;
|
|
15
|
-
private db: Db | null = null;
|
|
16
|
-
private url: string | null = null;
|
|
17
|
-
|
|
18
|
-
async connect(options: DatabaseOptions): Promise<Db> {
|
|
19
|
-
if (this.db) return this.db;
|
|
20
|
-
|
|
21
|
-
this.url = options.url;
|
|
22
|
-
const clientOptions: MongoClientOptions = {
|
|
23
|
-
...options.options,
|
|
24
|
-
maxPoolSize: 10,
|
|
25
|
-
minPoolSize: 1,
|
|
26
|
-
serverSelectionTimeoutMS: 5000,
|
|
27
|
-
socketTimeoutMS: 45000,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
this.client = new MongoClient(this.url, clientOptions);
|
|
31
|
-
await this.client.connect();
|
|
32
|
-
this.db = this.client.db();
|
|
33
|
-
|
|
34
|
-
return this.db;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async connectTo(uri: string, dbName?: string, options?: MongoClientOptions): Promise<Db> {
|
|
38
|
-
if (this.db) return this.db;
|
|
39
|
-
|
|
40
|
-
const clientOptions: MongoClientOptions = {
|
|
41
|
-
...options,
|
|
42
|
-
maxPoolSize: 10,
|
|
43
|
-
minPoolSize: 1,
|
|
44
|
-
serverSelectionTimeoutMS: 5000,
|
|
45
|
-
socketTimeoutMS: 45000,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
this.client = new MongoClient(uri, clientOptions);
|
|
49
|
-
await this.client.connect();
|
|
50
|
-
this.db = this.client.db(dbName);
|
|
51
|
-
|
|
52
|
-
return this.db;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
getDb(): Db {
|
|
56
|
-
if (!this.db) {
|
|
57
|
-
throw new Error('Database not connected. Call connect() first.');
|
|
58
|
-
}
|
|
59
|
-
return this.db;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
getClient(): MongoClient {
|
|
63
|
-
if (!this.client) {
|
|
64
|
-
throw new Error('Database not connected. Call connect() first.');
|
|
65
|
-
}
|
|
66
|
-
return this.client;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
collection<T = any>(name: string): Collection<T> {
|
|
70
|
-
return this.getDb().collection<T>(name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async close(): Promise<void> {
|
|
74
|
-
if (this.client) {
|
|
75
|
-
await this.client.close();
|
|
76
|
-
this.client = null;
|
|
77
|
-
this.db = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
isConnected(): boolean {
|
|
82
|
-
return this.client !== null && this.db !== null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const databaseManager = new DatabaseManager();
|
|
87
|
-
|
|
88
|
-
export const database = {
|
|
89
|
-
connect: (options: DatabaseOptions) => databaseManager.connect(options),
|
|
90
|
-
connectTo: (uri: string, dbName?: string, options?: MongoClientOptions) =>
|
|
91
|
-
databaseManager.connectTo(uri, dbName, options),
|
|
92
|
-
getDb: () => databaseManager.getDb(),
|
|
93
|
-
getClient: () => databaseManager.getClient(),
|
|
94
|
-
collection: <T = any>(name: string) => databaseManager.collection<T>(name),
|
|
95
|
-
close: () => databaseManager.close(),
|
|
96
|
-
isConnected: () => databaseManager.isConnected(),
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const db = database;
|
|
100
|
-
|
|
101
|
-
export { DatabaseManager };
|
|
102
|
-
export default database;
|
package/src/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export { AuthService, createAuth, auth, Auth } from './auth';
|
|
2
|
-
export { QueueManager, createQueue, queue } from './queue';
|
|
3
|
-
export { notify, notification } from './notifications';
|
|
4
|
-
export { logger } from './logger';
|
|
5
|
-
export { rateLimit, createRateLimiter } from './rate-limit';
|
|
6
|
-
export { config } from './config';
|
|
7
|
-
export { ResponseHelper, response } from './response';
|
|
8
|
-
export { upload, s3Service, S3Service } from './upload';
|
|
9
|
-
export { createApp, createExpressApp, SaaSAppBuilder, PluginManager, Plugin, AppOptions } from './plugin';
|
|
10
|
-
export { database, db, DatabaseManager, DatabaseConfig, DatabaseOptions } from './database';
|
|
11
|
-
|
|
12
|
-
export { AuthOptions, User, JWTPayload, TokenPair, LoginCredentials, RegisterData, Role, Permission, RolePermissions } from './auth/types';
|
|
13
|
-
export { QueueOptions, JobData, JobProcessor } from './queue';
|
|
14
|
-
export { EmailOptions, SMSOptions, WebhookOptions, SlackOptions } from './notifications';
|
|
15
|
-
export { LoggerConfig, LogLevel } from './logger';
|
|
16
|
-
export { RateLimitOptions } from './rate-limit';
|
|
17
|
-
export { EnvConfig, ConfigOptions } from './config';
|
|
18
|
-
export { ApiResponse, PaginatedResponse, ErrorResponse } from './response';
|
|
19
|
-
export { S3Config, UploadOptions, UploadResult, SignedUrlOptions, FileObject } from './upload';
|
package/src/logger/index.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import pino, { Logger, LoggerOptions, Bindings } from 'pino';
|
|
2
|
-
import { config } from '../config';
|
|
3
|
-
|
|
4
|
-
export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
5
|
-
|
|
6
|
-
export interface LoggerConfig extends Partial<LoggerOptions> {
|
|
7
|
-
level?: LogLevel;
|
|
8
|
-
name?: string;
|
|
9
|
-
prettyPrint?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface RequestLoggerOptions {
|
|
13
|
-
logLevel?: LogLevel;
|
|
14
|
-
autoLogging?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class LoggerManager {
|
|
18
|
-
private loggers: Map<string, Logger> = new Map();
|
|
19
|
-
private defaultLogger: Logger;
|
|
20
|
-
|
|
21
|
-
constructor() {
|
|
22
|
-
const level = (config.get('LOG_LEVEL') as LogLevel) || 'info';
|
|
23
|
-
this.defaultLogger = pino({
|
|
24
|
-
level,
|
|
25
|
-
name: 'saas-backend-kit',
|
|
26
|
-
formatters: {
|
|
27
|
-
bindings: (bindings: Bindings) => ({
|
|
28
|
-
...bindings,
|
|
29
|
-
service: 'saas-backend-kit',
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
createLogger(options: LoggerConfig = {}): Logger {
|
|
36
|
-
const name = options.name || 'default';
|
|
37
|
-
|
|
38
|
-
if (this.loggers.has(name)) {
|
|
39
|
-
return this.loggers.get(name)!;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const level = options.level || (config.get('LOG_LEVEL') as LogLevel) || 'info';
|
|
43
|
-
|
|
44
|
-
const logger = pino({
|
|
45
|
-
level,
|
|
46
|
-
name: options.name,
|
|
47
|
-
...options,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
this.loggers.set(name, logger);
|
|
51
|
-
return logger;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
getLogger(name?: string): Logger {
|
|
55
|
-
if (name) {
|
|
56
|
-
return this.loggers.get(name) || this.defaultLogger;
|
|
57
|
-
}
|
|
58
|
-
return this.defaultLogger;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
child(bindings: Bindings, options?: { name?: string }): Logger {
|
|
62
|
-
const name = options?.name || 'child';
|
|
63
|
-
const parent = options?.name ? this.getLogger(name) : this.defaultLogger;
|
|
64
|
-
return parent.child(bindings);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const loggerManager = new LoggerManager();
|
|
69
|
-
|
|
70
|
-
export const logger = {
|
|
71
|
-
info: (message: string, ...args: unknown[]) => loggerManager.getLogger().info(message, ...args),
|
|
72
|
-
warn: (message: string, ...args: unknown[]) => loggerManager.getLogger().warn(message, ...args),
|
|
73
|
-
error: (message: string, ...args: unknown[]) => loggerManager.getLogger().error(message, ...args),
|
|
74
|
-
debug: (message: string, ...args: unknown[]) => loggerManager.getLogger().debug(message, ...args),
|
|
75
|
-
trace: (message: string, ...args: unknown[]) => loggerManager.getLogger().trace(message, ...args),
|
|
76
|
-
fatal: (message: string, ...args: unknown[]) => loggerManager.getLogger().fatal(message, ...args),
|
|
77
|
-
child: (bindings: Bindings, options?: { name?: string }) => loggerManager.child(bindings, options),
|
|
78
|
-
create: (options?: LoggerConfig) => loggerManager.createLogger(options),
|
|
79
|
-
get: (name?: string) => loggerManager.getLogger(name),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export function createRequestLogger(options: RequestLoggerOptions = {}) {
|
|
83
|
-
const logLevel = options.logLevel || 'info';
|
|
84
|
-
const logger = loggerManager.getLogger('http');
|
|
85
|
-
|
|
86
|
-
return function requestLogger(
|
|
87
|
-
req: { method: string; url: string; headers: Record<string, string | string[] | undefined> },
|
|
88
|
-
res: { statusCode: number; statusMessage?: string },
|
|
89
|
-
elapsed: number
|
|
90
|
-
) {
|
|
91
|
-
const log = logger.child({
|
|
92
|
-
method: req.method,
|
|
93
|
-
url: req.url,
|
|
94
|
-
status: res.statusCode,
|
|
95
|
-
responseTime: elapsed,
|
|
96
|
-
ip: req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || 'unknown',
|
|
97
|
-
userAgent: req.headers['user-agent'],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (res.statusCode >= 500) {
|
|
101
|
-
log.error(`Request completed`);
|
|
102
|
-
} else if (res.statusCode >= 400) {
|
|
103
|
-
log.warn(`Request completed`);
|
|
104
|
-
} else {
|
|
105
|
-
log.info(`Request completed`);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default logger;
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import nodemailer, { Transporter, SendMailOptions } from 'nodemailer';
|
|
2
|
-
import { config } from '../config';
|
|
3
|
-
import { logger } from '../logger';
|
|
4
|
-
|
|
5
|
-
export interface EmailOptions {
|
|
6
|
-
to: string | string[];
|
|
7
|
-
subject: string;
|
|
8
|
-
text?: string;
|
|
9
|
-
html?: string;
|
|
10
|
-
template?: string;
|
|
11
|
-
templateData?: Record<string, unknown>;
|
|
12
|
-
from?: string;
|
|
13
|
-
cc?: string | string[];
|
|
14
|
-
bcc?: string | string[];
|
|
15
|
-
attachments?: Array<{
|
|
16
|
-
filename: string;
|
|
17
|
-
content?: Buffer | string;
|
|
18
|
-
path?: string;
|
|
19
|
-
contentType?: string;
|
|
20
|
-
}>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SMSOptions {
|
|
24
|
-
to: string;
|
|
25
|
-
message: string;
|
|
26
|
-
from?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface WebhookOptions {
|
|
30
|
-
url: string;
|
|
31
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH';
|
|
32
|
-
headers?: Record<string, string>;
|
|
33
|
-
body?: unknown;
|
|
34
|
-
timeout?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface SlackOptions {
|
|
38
|
-
text?: string;
|
|
39
|
-
blocks?: Array<{
|
|
40
|
-
type: string;
|
|
41
|
-
text?: { type: string; text: string; emoji?: boolean };
|
|
42
|
-
elements?: Array<{ type: string; text?: { type: string; text: string } }>;
|
|
43
|
-
accessory?: { type: string; image_url?: string; alt_text?: string };
|
|
44
|
-
}>;
|
|
45
|
-
channel?: string;
|
|
46
|
-
username?: string;
|
|
47
|
-
iconEmoji?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface NotificationTransporter {
|
|
51
|
-
email: Transporter;
|
|
52
|
-
twilio?: {
|
|
53
|
-
accountSid: string;
|
|
54
|
-
authToken: string;
|
|
55
|
-
phoneNumber: string;
|
|
56
|
-
};
|
|
57
|
-
slack?: {
|
|
58
|
-
webhookUrl: string;
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
class EmailService {
|
|
63
|
-
private transporter: Transporter | null = null;
|
|
64
|
-
private from: string;
|
|
65
|
-
|
|
66
|
-
constructor() {
|
|
67
|
-
this.from = config.get('SMTP_FROM') || 'noreply@example.com';
|
|
68
|
-
this.initialize();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private initialize(): void {
|
|
72
|
-
const host = config.get('SMTP_HOST');
|
|
73
|
-
const port = parseInt(config.get('SMTP_PORT') || '587', 10);
|
|
74
|
-
const user = config.get('SMTP_USER');
|
|
75
|
-
const pass = config.get('SMTP_PASS');
|
|
76
|
-
|
|
77
|
-
if (host && user && pass) {
|
|
78
|
-
this.transporter = nodemailer.createTransport({
|
|
79
|
-
host,
|
|
80
|
-
port,
|
|
81
|
-
secure: port === 465,
|
|
82
|
-
auth: {
|
|
83
|
-
user,
|
|
84
|
-
pass,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
logger.info('Email service initialized');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
setTransporter(transporter: Transporter): void {
|
|
92
|
-
this.transporter = transporter;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async send(options: EmailOptions): Promise<{ messageId: string }> {
|
|
96
|
-
if (!this.transporter) {
|
|
97
|
-
logger.warn('Email transporter not configured, skipping email');
|
|
98
|
-
return { messageId: 'mock-message-id' };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const mailOptions: SendMailOptions = {
|
|
102
|
-
from: options.from || this.from,
|
|
103
|
-
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
104
|
-
subject: options.subject,
|
|
105
|
-
text: options.text,
|
|
106
|
-
html: options.html || this.renderTemplate(options.template, options.templateData),
|
|
107
|
-
cc: options.cc,
|
|
108
|
-
bcc: options.bcc,
|
|
109
|
-
attachments: options.attachments,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const result = await this.transporter.sendMail(mailOptions);
|
|
113
|
-
logger.info(`Email sent to ${options.to}`, { messageId: result.messageId });
|
|
114
|
-
return { messageId: result.messageId };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private renderTemplate(templateName?: string, data?: Record<string, unknown>): string | undefined {
|
|
118
|
-
if (!templateName || !data) return undefined;
|
|
119
|
-
|
|
120
|
-
const templates: Record<string, (data: Record<string, unknown>) => string> = {
|
|
121
|
-
welcome: (data) => `
|
|
122
|
-
<h1>Welcome, ${data.name || 'User'}!</h1>
|
|
123
|
-
<p>Thank you for joining ${data.appName || 'our platform'}.</p>
|
|
124
|
-
<p>Get started by verifying your email.</p>
|
|
125
|
-
`,
|
|
126
|
-
passwordReset: (data) => `
|
|
127
|
-
<h1>Password Reset</h1>
|
|
128
|
-
<p>Click <a href="${data.resetUrl}">here</a> to reset your password.</p>
|
|
129
|
-
<p>This link expires in ${data.expiry || '1 hour'}.</p>
|
|
130
|
-
`,
|
|
131
|
-
verification: (data) => `
|
|
132
|
-
<h1>Verify Your Email</h1>
|
|
133
|
-
<p>Click <a href="${data.verifyUrl}">here</a> to verify your email.</p>
|
|
134
|
-
`,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const template = templates[templateName];
|
|
138
|
-
return template ? template(data) : undefined;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
class SMSService {
|
|
143
|
-
private accountSid: string;
|
|
144
|
-
private authToken: string;
|
|
145
|
-
private phoneNumber: string;
|
|
146
|
-
private initialized: boolean = false;
|
|
147
|
-
|
|
148
|
-
constructor() {
|
|
149
|
-
this.accountSid = config.get('TWILIO_ACCOUNT_SID') || '';
|
|
150
|
-
this.authToken = config.get('TWILIO_AUTH_TOKEN') || '';
|
|
151
|
-
this.phoneNumber = config.get('TWILIO_PHONE_NUMBER') || '';
|
|
152
|
-
this.initialized = !!(this.accountSid && this.authToken && this.phoneNumber);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async send(options: SMSOptions): Promise<{ sid: string }> {
|
|
156
|
-
if (!this.initialized) {
|
|
157
|
-
logger.warn('Twilio not configured, skipping SMS');
|
|
158
|
-
return { sid: 'mock-sid' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const twilio = await import('twilio');
|
|
163
|
-
const client = twilio.default(this.accountSid, this.authToken);
|
|
164
|
-
|
|
165
|
-
const result = await client.messages.create({
|
|
166
|
-
body: options.message,
|
|
167
|
-
from: options.from || this.phoneNumber,
|
|
168
|
-
to: options.to,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
logger.info(`SMS sent to ${options.to}`, { sid: result.sid });
|
|
172
|
-
return { sid: result.sid };
|
|
173
|
-
} catch (error) {
|
|
174
|
-
logger.error('Failed to send SMS', { error });
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
class WebhookService {
|
|
181
|
-
async send(options: WebhookOptions): Promise<{ status: number; body: unknown }> {
|
|
182
|
-
const response = await fetch(options.url, {
|
|
183
|
-
method: options.method || 'POST',
|
|
184
|
-
headers: {
|
|
185
|
-
'Content-Type': 'application/json',
|
|
186
|
-
...options.headers,
|
|
187
|
-
},
|
|
188
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
189
|
-
signal: options.timeout ? AbortSignal.timeout(options.timeout) : undefined,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const body = await response.json().catch(() => null);
|
|
193
|
-
logger.info(`Webhook sent to ${options.url}`, { status: response.status });
|
|
194
|
-
return { status: response.status, body };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
class SlackService {
|
|
199
|
-
private webhookUrl: string;
|
|
200
|
-
|
|
201
|
-
constructor() {
|
|
202
|
-
this.webhookUrl = config.get('SLACK_WEBHOOK_URL') || '';
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
setWebhookUrl(url: string): void {
|
|
206
|
-
this.webhookUrl = url;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async send(options: SlackOptions): Promise<{ ok: boolean }> {
|
|
210
|
-
if (!this.webhookUrl) {
|
|
211
|
-
logger.warn('Slack webhook not configured, skipping message');
|
|
212
|
-
return { ok: false };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const payload = {
|
|
216
|
-
text: options.text,
|
|
217
|
-
blocks: options.blocks,
|
|
218
|
-
channel: options.channel,
|
|
219
|
-
username: options.username,
|
|
220
|
-
icon_emoji: options.iconEmoji,
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const response = await fetch(this.webhookUrl, {
|
|
224
|
-
method: 'POST',
|
|
225
|
-
headers: { 'Content-Type': 'application/json' },
|
|
226
|
-
body: JSON.stringify(payload),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const ok = response.ok;
|
|
230
|
-
if (ok) {
|
|
231
|
-
logger.info('Slack message sent');
|
|
232
|
-
} else {
|
|
233
|
-
logger.error('Failed to send Slack message');
|
|
234
|
-
}
|
|
235
|
-
return { ok };
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
class NotificationService {
|
|
240
|
-
email: EmailService;
|
|
241
|
-
sms: SMSService;
|
|
242
|
-
webhook: WebhookService;
|
|
243
|
-
slack: SlackService;
|
|
244
|
-
|
|
245
|
-
constructor() {
|
|
246
|
-
this.email = new EmailService();
|
|
247
|
-
this.sms = new SMSService();
|
|
248
|
-
this.webhook = new WebhookService();
|
|
249
|
-
this.slack = new SlackService();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export const notify = new NotificationService();
|
|
254
|
-
|
|
255
|
-
export const notification = {
|
|
256
|
-
email: (options: EmailOptions) => notify.email.send(options),
|
|
257
|
-
sms: (options: SMSOptions) => notify.sms.send(options),
|
|
258
|
-
webhook: (options: WebhookOptions) => notify.webhook.send(options),
|
|
259
|
-
slack: (options: SlackOptions) => notify.slack.send(options),
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
export default notification;
|