saas-backend-kit 1.0.0
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/CHANGELOG.md +31 -0
- package/PUBLISHING.md +133 -0
- package/README.md +459 -0
- package/copy-dts.js +255 -0
- package/dist/auth/index.d.ts +58 -0
- package/dist/auth/index.js +584 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +569 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/config/index.d.ts +22 -0
- package/dist/config/index.js +106 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +100 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1303 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1281 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger/index.d.ts +18 -0
- package/dist/logger/index.js +188 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/index.mjs +178 -0
- package/dist/logger/index.mjs.map +1 -0
- package/dist/notifications/index.d.ts +35 -0
- package/dist/notifications/index.js +339 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/index.mjs +328 -0
- package/dist/notifications/index.mjs.map +1 -0
- package/dist/queue/index.d.ts +33 -0
- package/dist/queue/index.js +306 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/index.mjs +293 -0
- package/dist/queue/index.mjs.map +1 -0
- package/dist/rate-limit/index.d.ts +11 -0
- package/dist/rate-limit/index.js +290 -0
- package/dist/rate-limit/index.js.map +1 -0
- package/dist/rate-limit/index.mjs +286 -0
- package/dist/rate-limit/index.mjs.map +1 -0
- package/dist/response/index.d.ts +29 -0
- package/dist/response/index.js +120 -0
- package/dist/response/index.js.map +1 -0
- package/dist/response/index.mjs +114 -0
- package/dist/response/index.mjs.map +1 -0
- package/examples/express/.env.example +41 -0
- package/examples/express/app.ts +203 -0
- package/package.json +109 -0
- package/src/auth/express.ts +250 -0
- package/src/auth/fastify.ts +65 -0
- package/src/auth/index.ts +6 -0
- package/src/auth/jwt.ts +47 -0
- package/src/auth/oauth.ts +117 -0
- package/src/auth/rbac.ts +82 -0
- package/src/auth/types.ts +69 -0
- package/src/config/index.ts +120 -0
- package/src/index.ts +16 -0
- package/src/logger/index.ts +110 -0
- package/src/notifications/index.ts +262 -0
- package/src/plugin.ts +192 -0
- package/src/queue/index.ts +208 -0
- package/src/rate-limit/express.ts +144 -0
- package/src/rate-limit/fastify.ts +47 -0
- package/src/rate-limit/index.ts +2 -0
- package/src/response/index.ts +197 -0
- package/src/utils/index.ts +180 -0
- package/tsconfig.json +30 -0
- package/tsup.config.ts +24 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import { Queue, Worker } from 'bullmq';
|
|
4
|
+
import jwt from 'jsonwebtoken';
|
|
5
|
+
import bcrypt from 'bcryptjs';
|
|
6
|
+
import nodemailer from 'nodemailer';
|
|
7
|
+
import { Response } from 'express';
|
|
8
|
+
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
12
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
13
|
+
}) : x)(function(x) {
|
|
14
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
15
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
16
|
+
});
|
|
17
|
+
var __esm = (fn, res) => function __init() {
|
|
18
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/auth/types.ts
|
|
26
|
+
var DEFAULT_PERMISSIONS;
|
|
27
|
+
var init_types = __esm({
|
|
28
|
+
"src/auth/types.ts"() {
|
|
29
|
+
DEFAULT_PERMISSIONS = {
|
|
30
|
+
admin: ["*"],
|
|
31
|
+
user: ["read", "write:own"],
|
|
32
|
+
guest: ["read:public"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
var envSchema, ConfigManager, globalConfig, config;
|
|
37
|
+
var init_config = __esm({
|
|
38
|
+
"src/config/index.ts"() {
|
|
39
|
+
envSchema = z.object({
|
|
40
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
41
|
+
PORT: z.string().default("3000"),
|
|
42
|
+
DATABASE_URL: z.string().optional(),
|
|
43
|
+
REDIS_URL: z.string().default("redis://localhost:6379"),
|
|
44
|
+
JWT_SECRET: z.string().min(32).optional(),
|
|
45
|
+
JWT_EXPIRES_IN: z.string().default("7d"),
|
|
46
|
+
JWT_REFRESH_SECRET: z.string().min(32).optional(),
|
|
47
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default("30d"),
|
|
48
|
+
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
49
|
+
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
50
|
+
GOOGLE_REDIRECT_URI: z.string().optional(),
|
|
51
|
+
SMTP_HOST: z.string().optional(),
|
|
52
|
+
SMTP_PORT: z.string().default("587"),
|
|
53
|
+
SMTP_USER: z.string().optional(),
|
|
54
|
+
SMTP_PASS: z.string().optional(),
|
|
55
|
+
SMTP_FROM: z.string().optional(),
|
|
56
|
+
TWILIO_ACCOUNT_SID: z.string().optional(),
|
|
57
|
+
TWILIO_AUTH_TOKEN: z.string().optional(),
|
|
58
|
+
TWILIO_PHONE_NUMBER: z.string().optional(),
|
|
59
|
+
SLACK_WEBHOOK_URL: z.string().optional(),
|
|
60
|
+
RATE_LIMIT_WINDOW: z.string().default("1m"),
|
|
61
|
+
RATE_LIMIT_LIMIT: z.string().default("100"),
|
|
62
|
+
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
|
|
63
|
+
});
|
|
64
|
+
ConfigManager = class {
|
|
65
|
+
config = null;
|
|
66
|
+
schema;
|
|
67
|
+
validate;
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
this.schema = options.schema || envSchema;
|
|
70
|
+
this.validate = options.validate ?? true;
|
|
71
|
+
}
|
|
72
|
+
load() {
|
|
73
|
+
if (this.config) return this.config;
|
|
74
|
+
const env = {};
|
|
75
|
+
for (const key of Object.keys(this.schema.shape)) {
|
|
76
|
+
env[key] = process.env[key];
|
|
77
|
+
}
|
|
78
|
+
if (this.validate) {
|
|
79
|
+
const result = this.schema.safeParse(env);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
82
|
+
throw new Error(`Config validation failed: ${errors}`);
|
|
83
|
+
}
|
|
84
|
+
this.config = result.data;
|
|
85
|
+
} else {
|
|
86
|
+
this.config = env;
|
|
87
|
+
}
|
|
88
|
+
return this.config;
|
|
89
|
+
}
|
|
90
|
+
get(key) {
|
|
91
|
+
if (!this.config) this.load();
|
|
92
|
+
return this.config[key];
|
|
93
|
+
}
|
|
94
|
+
int(key) {
|
|
95
|
+
const value = this.get(key);
|
|
96
|
+
if (typeof value === "string") return parseInt(value, 10);
|
|
97
|
+
return Number(value);
|
|
98
|
+
}
|
|
99
|
+
bool(key) {
|
|
100
|
+
const value = this.get(key);
|
|
101
|
+
if (typeof value === "boolean") return value;
|
|
102
|
+
if (typeof value === "string") return value.toLowerCase() === "true";
|
|
103
|
+
return Boolean(value);
|
|
104
|
+
}
|
|
105
|
+
isProduction() {
|
|
106
|
+
return this.get("NODE_ENV") === "production";
|
|
107
|
+
}
|
|
108
|
+
isDevelopment() {
|
|
109
|
+
return this.get("NODE_ENV") === "development";
|
|
110
|
+
}
|
|
111
|
+
isTest() {
|
|
112
|
+
return this.get("NODE_ENV") === "test";
|
|
113
|
+
}
|
|
114
|
+
getAll() {
|
|
115
|
+
if (!this.config) this.load();
|
|
116
|
+
return this.config;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
globalConfig = new ConfigManager();
|
|
120
|
+
config = {
|
|
121
|
+
load: () => globalConfig.load(),
|
|
122
|
+
get: (key) => globalConfig.get(key),
|
|
123
|
+
int: (key) => globalConfig.int(key),
|
|
124
|
+
bool: (key) => globalConfig.bool(key),
|
|
125
|
+
isProduction: () => globalConfig.isProduction(),
|
|
126
|
+
isDevelopment: () => globalConfig.isDevelopment(),
|
|
127
|
+
isTest: () => globalConfig.isTest(),
|
|
128
|
+
getAll: () => globalConfig.getAll(),
|
|
129
|
+
create: (options) => new ConfigManager(options)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// src/auth/rbac.ts
|
|
135
|
+
var RBACService, rbacService;
|
|
136
|
+
var init_rbac = __esm({
|
|
137
|
+
"src/auth/rbac.ts"() {
|
|
138
|
+
init_types();
|
|
139
|
+
RBACService = class {
|
|
140
|
+
permissions;
|
|
141
|
+
constructor(permissions = DEFAULT_PERMISSIONS) {
|
|
142
|
+
this.permissions = permissions;
|
|
143
|
+
}
|
|
144
|
+
setPermissions(permissions) {
|
|
145
|
+
this.permissions = permissions;
|
|
146
|
+
}
|
|
147
|
+
addPermission(role, permission) {
|
|
148
|
+
if (!this.permissions[role]) {
|
|
149
|
+
this.permissions[role] = [];
|
|
150
|
+
}
|
|
151
|
+
if (!this.permissions[role].includes(permission)) {
|
|
152
|
+
this.permissions[role].push(permission);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
removePermission(role, permission) {
|
|
156
|
+
if (this.permissions[role]) {
|
|
157
|
+
this.permissions[role] = this.permissions[role].filter((p) => p !== permission);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
getPermissions(role) {
|
|
161
|
+
return this.permissions[role] || [];
|
|
162
|
+
}
|
|
163
|
+
hasPermission(role, requiredPermission) {
|
|
164
|
+
const rolePermissions = this.getPermissions(role);
|
|
165
|
+
if (rolePermissions.includes("*")) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (rolePermissions.includes(requiredPermission)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const [requiredAction, requiredScope] = requiredPermission.split(":");
|
|
172
|
+
for (const perm of rolePermissions) {
|
|
173
|
+
const [action, scope] = perm.split(":");
|
|
174
|
+
if (action === "*" && (scope === "*" || scope === requiredScope)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (action === requiredAction && (scope === "*" || scope === requiredScope)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
hasRole(userRole, requiredRole) {
|
|
184
|
+
const hierarchy = ["guest", "user", "admin"];
|
|
185
|
+
const userIndex = hierarchy.indexOf(userRole);
|
|
186
|
+
const requiredIndex = hierarchy.indexOf(requiredRole);
|
|
187
|
+
if (userIndex === -1 || requiredIndex === -1) {
|
|
188
|
+
return userRole === requiredRole;
|
|
189
|
+
}
|
|
190
|
+
return userIndex >= requiredIndex;
|
|
191
|
+
}
|
|
192
|
+
authorize(permission) {
|
|
193
|
+
return (role) => this.hasPermission(role, permission);
|
|
194
|
+
}
|
|
195
|
+
authorizeRole(requiredRole) {
|
|
196
|
+
return (role) => this.hasRole(role, requiredRole);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
rbacService = new RBACService();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
var LoggerManager, loggerManager, logger;
|
|
203
|
+
var init_logger = __esm({
|
|
204
|
+
"src/logger/index.ts"() {
|
|
205
|
+
init_config();
|
|
206
|
+
LoggerManager = class {
|
|
207
|
+
loggers = /* @__PURE__ */ new Map();
|
|
208
|
+
defaultLogger;
|
|
209
|
+
constructor() {
|
|
210
|
+
const level = config.get("LOG_LEVEL") || "info";
|
|
211
|
+
this.defaultLogger = pino({
|
|
212
|
+
level,
|
|
213
|
+
name: "saas-backend-kit",
|
|
214
|
+
formatters: {
|
|
215
|
+
bindings: (bindings) => ({
|
|
216
|
+
...bindings,
|
|
217
|
+
service: "saas-backend-kit"
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
createLogger(options = {}) {
|
|
223
|
+
const name = options.name || "default";
|
|
224
|
+
if (this.loggers.has(name)) {
|
|
225
|
+
return this.loggers.get(name);
|
|
226
|
+
}
|
|
227
|
+
const level = options.level || config.get("LOG_LEVEL") || "info";
|
|
228
|
+
const logger2 = pino({
|
|
229
|
+
level,
|
|
230
|
+
name: options.name,
|
|
231
|
+
...options
|
|
232
|
+
});
|
|
233
|
+
this.loggers.set(name, logger2);
|
|
234
|
+
return logger2;
|
|
235
|
+
}
|
|
236
|
+
getLogger(name) {
|
|
237
|
+
if (name) {
|
|
238
|
+
return this.loggers.get(name) || this.defaultLogger;
|
|
239
|
+
}
|
|
240
|
+
return this.defaultLogger;
|
|
241
|
+
}
|
|
242
|
+
child(bindings, options) {
|
|
243
|
+
const name = options?.name || "child";
|
|
244
|
+
const parent = options?.name ? this.getLogger(name) : this.defaultLogger;
|
|
245
|
+
return parent.child(bindings);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
loggerManager = new LoggerManager();
|
|
249
|
+
logger = {
|
|
250
|
+
info: (message, ...args) => loggerManager.getLogger().info(message, ...args),
|
|
251
|
+
warn: (message, ...args) => loggerManager.getLogger().warn(message, ...args),
|
|
252
|
+
error: (message, ...args) => loggerManager.getLogger().error(message, ...args),
|
|
253
|
+
debug: (message, ...args) => loggerManager.getLogger().debug(message, ...args),
|
|
254
|
+
trace: (message, ...args) => loggerManager.getLogger().trace(message, ...args),
|
|
255
|
+
fatal: (message, ...args) => loggerManager.getLogger().fatal(message, ...args),
|
|
256
|
+
child: (bindings, options) => loggerManager.child(bindings, options),
|
|
257
|
+
create: (options) => loggerManager.createLogger(options),
|
|
258
|
+
get: (name) => loggerManager.getLogger(name)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/queue/index.ts
|
|
264
|
+
var queue_exports = {};
|
|
265
|
+
__export(queue_exports, {
|
|
266
|
+
QueueManager: () => QueueManager,
|
|
267
|
+
addJob: () => addJob,
|
|
268
|
+
createQueue: () => createQueue,
|
|
269
|
+
default: () => queue_default,
|
|
270
|
+
processJob: () => processJob,
|
|
271
|
+
queue: () => queue
|
|
272
|
+
});
|
|
273
|
+
var QueueManager, queueManager, createQueue, addJob, processJob, queue, queue_default;
|
|
274
|
+
var init_queue = __esm({
|
|
275
|
+
"src/queue/index.ts"() {
|
|
276
|
+
init_config();
|
|
277
|
+
init_logger();
|
|
278
|
+
QueueManager = class {
|
|
279
|
+
queues = /* @__PURE__ */ new Map();
|
|
280
|
+
workers = /* @__PURE__ */ new Map();
|
|
281
|
+
redisOptions;
|
|
282
|
+
constructor() {
|
|
283
|
+
const redisUrl = config.get("REDIS_URL");
|
|
284
|
+
if (redisUrl) {
|
|
285
|
+
this.redisOptions = { url: redisUrl };
|
|
286
|
+
} else {
|
|
287
|
+
this.redisOptions = {
|
|
288
|
+
host: "localhost",
|
|
289
|
+
port: 6379
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
setRedisOptions(options) {
|
|
294
|
+
this.redisOptions = options;
|
|
295
|
+
}
|
|
296
|
+
createQueue(name, options) {
|
|
297
|
+
if (this.queues.has(name)) {
|
|
298
|
+
return this.queues.get(name);
|
|
299
|
+
}
|
|
300
|
+
const queue2 = new Queue(name, {
|
|
301
|
+
connection: this.redisOptions,
|
|
302
|
+
defaultJobOptions: options?.defaultJobOptions || {
|
|
303
|
+
removeOnComplete: 100,
|
|
304
|
+
removeOnFail: 100
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
this.queues.set(name, queue2);
|
|
308
|
+
logger.info(`Queue "${name}" created`);
|
|
309
|
+
return queue2;
|
|
310
|
+
}
|
|
311
|
+
getQueue(name) {
|
|
312
|
+
return this.queues.get(name);
|
|
313
|
+
}
|
|
314
|
+
async addJob(queueName, jobName, data, options) {
|
|
315
|
+
const queue2 = this.getQueue(queueName) || this.createQueue(queueName);
|
|
316
|
+
const job = await queue2.add(jobName, data, options);
|
|
317
|
+
logger.debug(`Job "${jobName}" added to queue "${queueName}"`, { jobId: job.id });
|
|
318
|
+
return job;
|
|
319
|
+
}
|
|
320
|
+
async addBulkJobs(queueName, jobs) {
|
|
321
|
+
const queue2 = this.getQueue(queueName) || this.createQueue(queueName);
|
|
322
|
+
const bulkJobs = jobs.map((job) => ({
|
|
323
|
+
name: job.name,
|
|
324
|
+
data: job.data,
|
|
325
|
+
...job.options
|
|
326
|
+
}));
|
|
327
|
+
const result = await queue2.addBulk(bulkJobs);
|
|
328
|
+
logger.debug(`${jobs.length} jobs added to queue "${queueName}"`);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
processJob(queueName, processor, options) {
|
|
332
|
+
this.getQueue(queueName) || this.createQueue(queueName);
|
|
333
|
+
const worker = new Worker(queueName, async (job) => {
|
|
334
|
+
logger.debug(`Processing job "${job.name}"`, { jobId: job.id, queue: queueName });
|
|
335
|
+
return await processor(job);
|
|
336
|
+
}, {
|
|
337
|
+
connection: this.redisOptions,
|
|
338
|
+
concurrency: options?.concurrency || 1,
|
|
339
|
+
...options
|
|
340
|
+
});
|
|
341
|
+
worker.on("completed", (job) => {
|
|
342
|
+
logger.debug(`Job completed`, { jobId: job.id, queue: queueName });
|
|
343
|
+
});
|
|
344
|
+
worker.on("failed", (job, err) => {
|
|
345
|
+
logger.error(`Job failed`, { jobId: job?.id, queue: queueName, error: err.message });
|
|
346
|
+
});
|
|
347
|
+
worker.on("error", (err) => {
|
|
348
|
+
logger.error(`Worker error`, { queue: queueName, error: err.message });
|
|
349
|
+
});
|
|
350
|
+
this.workers.set(queueName, worker);
|
|
351
|
+
logger.info(`Worker started for queue "${queueName}"`);
|
|
352
|
+
return worker;
|
|
353
|
+
}
|
|
354
|
+
async getJobCounts(queueName) {
|
|
355
|
+
const queue2 = this.getQueue(queueName);
|
|
356
|
+
if (!queue2) {
|
|
357
|
+
return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
|
|
358
|
+
}
|
|
359
|
+
return await queue2.getJobCounts();
|
|
360
|
+
}
|
|
361
|
+
async getJobs(queueName, start = 0, end = 10) {
|
|
362
|
+
const queue2 = this.getQueue(queueName);
|
|
363
|
+
if (!queue2) return [];
|
|
364
|
+
return await queue2.getJobs(["waiting", "active", "completed", "failed"], start, end);
|
|
365
|
+
}
|
|
366
|
+
async closeQueue(name) {
|
|
367
|
+
const queue2 = this.queues.get(name);
|
|
368
|
+
if (queue2) {
|
|
369
|
+
await queue2.close();
|
|
370
|
+
this.queues.delete(name);
|
|
371
|
+
logger.info(`Queue "${name}" closed`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async closeWorker(name) {
|
|
375
|
+
const worker = this.workers.get(name);
|
|
376
|
+
if (worker) {
|
|
377
|
+
await worker.close();
|
|
378
|
+
this.workers.delete(name);
|
|
379
|
+
logger.info(`Worker for queue "${name}" closed`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async closeAll() {
|
|
383
|
+
await Promise.all([
|
|
384
|
+
...Array.from(this.queues.values()).map((q) => q.close()),
|
|
385
|
+
...Array.from(this.workers.values()).map((w) => w.close())
|
|
386
|
+
]);
|
|
387
|
+
this.queues.clear();
|
|
388
|
+
this.workers.clear();
|
|
389
|
+
logger.info("All queues and workers closed");
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
queueManager = new QueueManager();
|
|
393
|
+
createQueue = (name, options) => {
|
|
394
|
+
return queueManager.createQueue(name, options);
|
|
395
|
+
};
|
|
396
|
+
addJob = (queueName, jobName, data, options) => {
|
|
397
|
+
return queueManager.addJob(queueName, jobName, data, options);
|
|
398
|
+
};
|
|
399
|
+
processJob = (queueName, processor, options) => {
|
|
400
|
+
return queueManager.processJob(queueName, processor, options);
|
|
401
|
+
};
|
|
402
|
+
queue = {
|
|
403
|
+
create: createQueue,
|
|
404
|
+
add: addJob,
|
|
405
|
+
process: processJob,
|
|
406
|
+
get: (name) => queueManager.getQueue(name),
|
|
407
|
+
getJobCounts: (name) => queueManager.getJobCounts(name),
|
|
408
|
+
getJobs: (name, start, end) => queueManager.getJobs(name, start, end),
|
|
409
|
+
close: (name) => queueManager.closeQueue(name),
|
|
410
|
+
closeAll: () => queueManager.closeAll(),
|
|
411
|
+
setRedisOptions: (options) => queueManager.setRedisOptions(options)
|
|
412
|
+
};
|
|
413
|
+
queue_default = queue;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// src/rate-limit/express.ts
|
|
418
|
+
function rateLimit(options = {}) {
|
|
419
|
+
const limiter = new RateLimiter(options);
|
|
420
|
+
return limiter.middleware();
|
|
421
|
+
}
|
|
422
|
+
function createRateLimiter(options) {
|
|
423
|
+
return new RateLimiter(options);
|
|
424
|
+
}
|
|
425
|
+
var InMemoryRateLimiter, RateLimiter;
|
|
426
|
+
var init_express = __esm({
|
|
427
|
+
"src/rate-limit/express.ts"() {
|
|
428
|
+
init_config();
|
|
429
|
+
InMemoryRateLimiter = class {
|
|
430
|
+
store = /* @__PURE__ */ new Map();
|
|
431
|
+
cleanupInterval;
|
|
432
|
+
constructor() {
|
|
433
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
|
|
434
|
+
}
|
|
435
|
+
cleanup() {
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
for (const [key, record] of this.store.entries()) {
|
|
438
|
+
if (record.resetTime < now) {
|
|
439
|
+
this.store.delete(key);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
increment(key, windowMs) {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const record = this.store.get(key);
|
|
446
|
+
if (!record || record.resetTime < now) {
|
|
447
|
+
const resetTime = now + windowMs;
|
|
448
|
+
this.store.set(key, { count: 1, resetTime });
|
|
449
|
+
return { count: 1, resetTime };
|
|
450
|
+
}
|
|
451
|
+
record.count++;
|
|
452
|
+
return record;
|
|
453
|
+
}
|
|
454
|
+
get(key) {
|
|
455
|
+
return this.store.get(key);
|
|
456
|
+
}
|
|
457
|
+
destroy() {
|
|
458
|
+
clearInterval(this.cleanupInterval);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
RateLimiter = class {
|
|
462
|
+
options;
|
|
463
|
+
store;
|
|
464
|
+
constructor(options = {}) {
|
|
465
|
+
const defaultWindow = config.get("RATE_LIMIT_WINDOW") || "1m";
|
|
466
|
+
const defaultLimit = parseInt(config.get("RATE_LIMIT_LIMIT") || "100", 10);
|
|
467
|
+
this.options = {
|
|
468
|
+
window: options.window || defaultWindow,
|
|
469
|
+
limit: options.limit || defaultLimit,
|
|
470
|
+
keyGenerator: options.keyGenerator || ((req) => req.ip || "unknown"),
|
|
471
|
+
handler: options.handler || this.defaultHandler,
|
|
472
|
+
skipSuccessfulRequests: options.skipSuccessfulRequests || false,
|
|
473
|
+
skipFailedRequests: options.skipFailedRequests || false,
|
|
474
|
+
skip: options.skip || (() => false)
|
|
475
|
+
};
|
|
476
|
+
this.store = new InMemoryRateLimiter();
|
|
477
|
+
}
|
|
478
|
+
getWindowMs() {
|
|
479
|
+
const window = this.options.window;
|
|
480
|
+
const match = window.match(/^(\d+)(s|m|h|d)$/);
|
|
481
|
+
if (!match) return 6e4;
|
|
482
|
+
const value = parseInt(match[1], 10);
|
|
483
|
+
const unit = match[2];
|
|
484
|
+
switch (unit) {
|
|
485
|
+
case "s":
|
|
486
|
+
return value * 1e3;
|
|
487
|
+
case "m":
|
|
488
|
+
return value * 60 * 1e3;
|
|
489
|
+
case "h":
|
|
490
|
+
return value * 60 * 60 * 1e3;
|
|
491
|
+
case "d":
|
|
492
|
+
return value * 24 * 60 * 60 * 1e3;
|
|
493
|
+
default:
|
|
494
|
+
return 6e4;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
defaultHandler(req, res) {
|
|
498
|
+
res.status(429).json({
|
|
499
|
+
error: "Too many requests",
|
|
500
|
+
message: `Rate limit exceeded. Please try again later.`
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
middleware() {
|
|
504
|
+
const windowMs = this.getWindowMs();
|
|
505
|
+
return (req, res, next) => {
|
|
506
|
+
if (this.options.skip(req)) {
|
|
507
|
+
return next();
|
|
508
|
+
}
|
|
509
|
+
const key = this.options.keyGenerator(req);
|
|
510
|
+
const record = this.store.increment(key, windowMs);
|
|
511
|
+
const remaining = Math.max(0, this.options.limit - record.count);
|
|
512
|
+
new Date(record.resetTime);
|
|
513
|
+
res.setHeader("X-RateLimit-Limit", this.options.limit);
|
|
514
|
+
res.setHeader("X-RateLimit-Remaining", remaining);
|
|
515
|
+
res.setHeader("X-RateLimit-Reset", Math.ceil(record.resetTime / 1e3));
|
|
516
|
+
if (record.count > this.options.limit) {
|
|
517
|
+
return this.options.handler(req, res);
|
|
518
|
+
}
|
|
519
|
+
next();
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
destroy() {
|
|
523
|
+
this.store.destroy();
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// src/auth/index.ts
|
|
530
|
+
init_types();
|
|
531
|
+
|
|
532
|
+
// src/auth/jwt.ts
|
|
533
|
+
init_config();
|
|
534
|
+
var JWTService = class {
|
|
535
|
+
secret;
|
|
536
|
+
expiresIn;
|
|
537
|
+
refreshSecret;
|
|
538
|
+
refreshExpiresIn;
|
|
539
|
+
constructor(options = {}) {
|
|
540
|
+
this.secret = options.jwtSecret || config.get("JWT_SECRET") || "default-secret-change-in-production";
|
|
541
|
+
this.expiresIn = options.jwtExpiresIn || config.get("JWT_EXPIRES_IN") || "7d";
|
|
542
|
+
this.refreshSecret = options.refreshSecret || config.get("JWT_REFRESH_SECRET") || this.secret;
|
|
543
|
+
this.refreshExpiresIn = options.refreshExpiresIn || config.get("JWT_REFRESH_EXPIRES_IN") || "30d";
|
|
544
|
+
}
|
|
545
|
+
generateToken(payload) {
|
|
546
|
+
return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
|
|
547
|
+
}
|
|
548
|
+
generateRefreshToken(payload) {
|
|
549
|
+
return jwt.sign(payload, this.refreshSecret, { expiresIn: this.refreshExpiresIn });
|
|
550
|
+
}
|
|
551
|
+
generateTokenPair(payload) {
|
|
552
|
+
return {
|
|
553
|
+
accessToken: this.generateToken(payload),
|
|
554
|
+
refreshToken: this.generateRefreshToken(payload)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
verifyToken(token) {
|
|
558
|
+
return jwt.verify(token, this.secret);
|
|
559
|
+
}
|
|
560
|
+
verifyRefreshToken(token) {
|
|
561
|
+
return jwt.verify(token, this.refreshSecret);
|
|
562
|
+
}
|
|
563
|
+
refreshTokens(refreshToken) {
|
|
564
|
+
const payload = this.verifyRefreshToken(refreshToken);
|
|
565
|
+
return this.generateTokenPair(payload);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var jwtService = new JWTService();
|
|
569
|
+
|
|
570
|
+
// src/auth/index.ts
|
|
571
|
+
init_rbac();
|
|
572
|
+
|
|
573
|
+
// src/auth/oauth.ts
|
|
574
|
+
init_config();
|
|
575
|
+
var OAuthService = class {
|
|
576
|
+
providers = /* @__PURE__ */ new Map();
|
|
577
|
+
constructor() {
|
|
578
|
+
const googleClientId = config.get("GOOGLE_CLIENT_ID");
|
|
579
|
+
const googleClientSecret = config.get("GOOGLE_CLIENT_SECRET");
|
|
580
|
+
const googleRedirectUri = config.get("GOOGLE_REDIRECT_URI");
|
|
581
|
+
if (googleClientId && googleClientSecret && googleRedirectUri) {
|
|
582
|
+
this.registerProvider("google", {
|
|
583
|
+
name: "google",
|
|
584
|
+
clientId: googleClientId,
|
|
585
|
+
clientSecret: googleClientSecret,
|
|
586
|
+
redirectUri: googleRedirectUri,
|
|
587
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
588
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
589
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
registerProvider(name, provider) {
|
|
594
|
+
this.providers.set(name, provider);
|
|
595
|
+
}
|
|
596
|
+
getProvider(name) {
|
|
597
|
+
return this.providers.get(name);
|
|
598
|
+
}
|
|
599
|
+
getAuthorizationUrl(providerName, state) {
|
|
600
|
+
const provider = this.getProvider(providerName);
|
|
601
|
+
if (!provider) {
|
|
602
|
+
throw new Error(`OAuth provider ${providerName} not registered`);
|
|
603
|
+
}
|
|
604
|
+
const params = new URLSearchParams({
|
|
605
|
+
client_id: provider.clientId,
|
|
606
|
+
redirect_uri: provider.redirectUri,
|
|
607
|
+
response_type: "code",
|
|
608
|
+
scope: "openid email profile",
|
|
609
|
+
state: state || ""
|
|
610
|
+
});
|
|
611
|
+
return `${provider.authorizationUrl}?${params.toString()}`;
|
|
612
|
+
}
|
|
613
|
+
async exchangeCode(providerName, code) {
|
|
614
|
+
const provider = this.getProvider(providerName);
|
|
615
|
+
if (!provider) {
|
|
616
|
+
throw new Error(`OAuth provider ${providerName} not registered`);
|
|
617
|
+
}
|
|
618
|
+
const response2 = await fetch(provider.tokenUrl, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: {
|
|
621
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
622
|
+
},
|
|
623
|
+
body: new URLSearchParams({
|
|
624
|
+
client_id: provider.clientId,
|
|
625
|
+
client_secret: provider.clientSecret,
|
|
626
|
+
code,
|
|
627
|
+
grant_type: "authorization_code",
|
|
628
|
+
redirect_uri: provider.redirectUri
|
|
629
|
+
})
|
|
630
|
+
});
|
|
631
|
+
if (!response2.ok) {
|
|
632
|
+
const error = await response2.text();
|
|
633
|
+
throw new Error(`OAuth token exchange failed: ${error}`);
|
|
634
|
+
}
|
|
635
|
+
const data = await response2.json();
|
|
636
|
+
return {
|
|
637
|
+
accessToken: data.access_token,
|
|
638
|
+
refreshToken: data.refresh_token
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async getUserInfo(providerName, accessToken) {
|
|
642
|
+
const provider = this.getProvider(providerName);
|
|
643
|
+
if (!provider) {
|
|
644
|
+
throw new Error(`OAuth provider ${providerName} not registered`);
|
|
645
|
+
}
|
|
646
|
+
const response2 = await fetch(provider.userInfoUrl, {
|
|
647
|
+
headers: {
|
|
648
|
+
Authorization: `Bearer ${accessToken}`
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
if (!response2.ok) {
|
|
652
|
+
const error = await response2.text();
|
|
653
|
+
throw new Error(`OAuth user info fetch failed: ${error}`);
|
|
654
|
+
}
|
|
655
|
+
const data = await response2.json();
|
|
656
|
+
return {
|
|
657
|
+
id: data.id,
|
|
658
|
+
email: data.email,
|
|
659
|
+
name: data.name,
|
|
660
|
+
picture: data.picture
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
var oauthService = new OAuthService();
|
|
665
|
+
init_rbac();
|
|
666
|
+
var InMemoryUserStore = class {
|
|
667
|
+
users = /* @__PURE__ */ new Map();
|
|
668
|
+
async findByEmail(email) {
|
|
669
|
+
for (const user of this.users.values()) {
|
|
670
|
+
if (user.email === email) return user;
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
async findById(id) {
|
|
675
|
+
return this.users.get(id) || null;
|
|
676
|
+
}
|
|
677
|
+
async create(data) {
|
|
678
|
+
const id = Math.random().toString(36).substr(2, 9);
|
|
679
|
+
const hashedPassword = await bcrypt.hash(data.password, 10);
|
|
680
|
+
const user = {
|
|
681
|
+
id,
|
|
682
|
+
email: data.email,
|
|
683
|
+
password: hashedPassword,
|
|
684
|
+
role: data.role || "user",
|
|
685
|
+
name: data.name
|
|
686
|
+
};
|
|
687
|
+
this.users.set(id, user);
|
|
688
|
+
return user;
|
|
689
|
+
}
|
|
690
|
+
async update(id, data) {
|
|
691
|
+
const user = this.users.get(id);
|
|
692
|
+
if (!user) throw new Error("User not found");
|
|
693
|
+
const updated = { ...user, ...data };
|
|
694
|
+
this.users.set(id, updated);
|
|
695
|
+
return updated;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var AuthService = class {
|
|
699
|
+
userStore;
|
|
700
|
+
options;
|
|
701
|
+
initialized = false;
|
|
702
|
+
constructor(options = {}, userStore) {
|
|
703
|
+
this.options = {
|
|
704
|
+
jwtSecret: options.jwtSecret || process.env.JWT_SECRET || "default-secret-change-in-production",
|
|
705
|
+
jwtExpiresIn: options.jwtExpiresIn || "7d",
|
|
706
|
+
refreshSecret: options.refreshSecret || process.env.JWT_REFRESH_SECRET || "default-secret-change-in-production",
|
|
707
|
+
refreshExpiresIn: options.refreshExpiresIn || "30d",
|
|
708
|
+
googleClientId: options.googleClientId || process.env.GOOGLE_CLIENT_ID || "",
|
|
709
|
+
googleClientSecret: options.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || "",
|
|
710
|
+
googleRedirectUri: options.googleRedirectUri || process.env.GOOGLE_REDIRECT_URI || ""
|
|
711
|
+
};
|
|
712
|
+
this.userStore = userStore || new InMemoryUserStore();
|
|
713
|
+
}
|
|
714
|
+
async initialize() {
|
|
715
|
+
if (this.options.googleClientId && this.options.googleClientSecret && this.options.googleRedirectUri) {
|
|
716
|
+
oauthService.registerProvider("google", {
|
|
717
|
+
name: "google",
|
|
718
|
+
clientId: this.options.googleClientId,
|
|
719
|
+
clientSecret: this.options.googleClientSecret,
|
|
720
|
+
redirectUri: this.options.googleRedirectUri,
|
|
721
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
722
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
723
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
this.initialized = true;
|
|
727
|
+
}
|
|
728
|
+
async register(data) {
|
|
729
|
+
const existing = await this.userStore.findByEmail(data.email);
|
|
730
|
+
if (existing) {
|
|
731
|
+
throw new Error("User already exists");
|
|
732
|
+
}
|
|
733
|
+
const user = await this.userStore.create(data);
|
|
734
|
+
const tokens = jwtService.generateTokenPair({
|
|
735
|
+
userId: user.id,
|
|
736
|
+
email: user.email,
|
|
737
|
+
role: user.role
|
|
738
|
+
});
|
|
739
|
+
return { user, tokens };
|
|
740
|
+
}
|
|
741
|
+
async login(credentials) {
|
|
742
|
+
const user = await this.userStore.findByEmail(credentials.email);
|
|
743
|
+
if (!user || !user.password) {
|
|
744
|
+
throw new Error("Invalid credentials");
|
|
745
|
+
}
|
|
746
|
+
const isValid = await bcrypt.compare(credentials.password, user.password);
|
|
747
|
+
if (!isValid) {
|
|
748
|
+
throw new Error("Invalid credentials");
|
|
749
|
+
}
|
|
750
|
+
const tokens = jwtService.generateTokenPair({
|
|
751
|
+
userId: user.id,
|
|
752
|
+
email: user.email,
|
|
753
|
+
role: user.role
|
|
754
|
+
});
|
|
755
|
+
return { user, tokens };
|
|
756
|
+
}
|
|
757
|
+
async refresh(refreshToken) {
|
|
758
|
+
return jwtService.refreshTokens(refreshToken);
|
|
759
|
+
}
|
|
760
|
+
async getGoogleAuthUrl(state) {
|
|
761
|
+
return oauthService.getAuthorizationUrl("google", state);
|
|
762
|
+
}
|
|
763
|
+
async handleGoogleCallback(code) {
|
|
764
|
+
const { accessToken } = await oauthService.exchangeCode("google", code);
|
|
765
|
+
const userInfo = await oauthService.getUserInfo("google", accessToken);
|
|
766
|
+
let user = await this.userStore.findByEmail(userInfo.email);
|
|
767
|
+
if (!user) {
|
|
768
|
+
user = await this.userStore.create({
|
|
769
|
+
email: userInfo.email,
|
|
770
|
+
password: Math.random().toString(36).substr(2, 20),
|
|
771
|
+
name: userInfo.name,
|
|
772
|
+
role: "user"
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const tokens = jwtService.generateTokenPair({
|
|
776
|
+
userId: user.id,
|
|
777
|
+
email: user.email,
|
|
778
|
+
role: user.role
|
|
779
|
+
});
|
|
780
|
+
return { user, tokens };
|
|
781
|
+
}
|
|
782
|
+
getMiddleware() {
|
|
783
|
+
return (req, res, next) => {
|
|
784
|
+
const authHeader = req.headers.authorization;
|
|
785
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
786
|
+
return res.status(401).json({ error: "No token provided" });
|
|
787
|
+
}
|
|
788
|
+
const token = authHeader.substring(7);
|
|
789
|
+
try {
|
|
790
|
+
const payload = jwtService.verifyToken(token);
|
|
791
|
+
req.user = {
|
|
792
|
+
...payload,
|
|
793
|
+
id: payload.userId
|
|
794
|
+
};
|
|
795
|
+
next();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
requireUser() {
|
|
802
|
+
return (req, res, next) => {
|
|
803
|
+
if (!req.user) {
|
|
804
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
805
|
+
}
|
|
806
|
+
next();
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
requireRole(role) {
|
|
810
|
+
return (req, res, next) => {
|
|
811
|
+
if (!req.user) {
|
|
812
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
813
|
+
}
|
|
814
|
+
if (!rbacService.hasRole(req.user.role, role)) {
|
|
815
|
+
return res.status(403).json({ error: "Insufficient permissions" });
|
|
816
|
+
}
|
|
817
|
+
next();
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
requirePermission(permission) {
|
|
821
|
+
return (req, res, next) => {
|
|
822
|
+
if (!req.user) {
|
|
823
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
824
|
+
}
|
|
825
|
+
if (!rbacService.hasPermission(req.user.role, permission)) {
|
|
826
|
+
return res.status(403).json({ error: "Insufficient permissions" });
|
|
827
|
+
}
|
|
828
|
+
next();
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
var authServiceInstance = null;
|
|
833
|
+
function createAuth(options, userStore) {
|
|
834
|
+
authServiceInstance = new AuthService(options, userStore);
|
|
835
|
+
return authServiceInstance;
|
|
836
|
+
}
|
|
837
|
+
function auth() {
|
|
838
|
+
if (!authServiceInstance) {
|
|
839
|
+
authServiceInstance = new AuthService();
|
|
840
|
+
}
|
|
841
|
+
return authServiceInstance;
|
|
842
|
+
}
|
|
843
|
+
var Auth = {
|
|
844
|
+
initialize: (options) => {
|
|
845
|
+
const service = createAuth(options);
|
|
846
|
+
return {
|
|
847
|
+
service,
|
|
848
|
+
getMiddleware: () => service.getMiddleware(),
|
|
849
|
+
requireUser: () => service.requireUser(),
|
|
850
|
+
requireRole: (role) => service.requireRole(role),
|
|
851
|
+
requirePermission: (permission) => service.requirePermission(permission)
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/index.ts
|
|
857
|
+
init_queue();
|
|
858
|
+
|
|
859
|
+
// src/notifications/index.ts
|
|
860
|
+
init_config();
|
|
861
|
+
init_logger();
|
|
862
|
+
var EmailService = class {
|
|
863
|
+
transporter = null;
|
|
864
|
+
from;
|
|
865
|
+
constructor() {
|
|
866
|
+
this.from = config.get("SMTP_FROM") || "noreply@example.com";
|
|
867
|
+
this.initialize();
|
|
868
|
+
}
|
|
869
|
+
initialize() {
|
|
870
|
+
const host = config.get("SMTP_HOST");
|
|
871
|
+
const port = parseInt(config.get("SMTP_PORT") || "587", 10);
|
|
872
|
+
const user = config.get("SMTP_USER");
|
|
873
|
+
const pass = config.get("SMTP_PASS");
|
|
874
|
+
if (host && user && pass) {
|
|
875
|
+
this.transporter = nodemailer.createTransport({
|
|
876
|
+
host,
|
|
877
|
+
port,
|
|
878
|
+
secure: port === 465,
|
|
879
|
+
auth: {
|
|
880
|
+
user,
|
|
881
|
+
pass
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
logger.info("Email service initialized");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
setTransporter(transporter) {
|
|
888
|
+
this.transporter = transporter;
|
|
889
|
+
}
|
|
890
|
+
async send(options) {
|
|
891
|
+
if (!this.transporter) {
|
|
892
|
+
logger.warn("Email transporter not configured, skipping email");
|
|
893
|
+
return { messageId: "mock-message-id" };
|
|
894
|
+
}
|
|
895
|
+
const mailOptions = {
|
|
896
|
+
from: options.from || this.from,
|
|
897
|
+
to: Array.isArray(options.to) ? options.to.join(", ") : options.to,
|
|
898
|
+
subject: options.subject,
|
|
899
|
+
text: options.text,
|
|
900
|
+
html: options.html || this.renderTemplate(options.template, options.templateData),
|
|
901
|
+
cc: options.cc,
|
|
902
|
+
bcc: options.bcc,
|
|
903
|
+
attachments: options.attachments
|
|
904
|
+
};
|
|
905
|
+
const result = await this.transporter.sendMail(mailOptions);
|
|
906
|
+
logger.info(`Email sent to ${options.to}`, { messageId: result.messageId });
|
|
907
|
+
return { messageId: result.messageId };
|
|
908
|
+
}
|
|
909
|
+
renderTemplate(templateName, data) {
|
|
910
|
+
if (!templateName || !data) return void 0;
|
|
911
|
+
const templates = {
|
|
912
|
+
welcome: (data2) => `
|
|
913
|
+
<h1>Welcome, ${data2.name || "User"}!</h1>
|
|
914
|
+
<p>Thank you for joining ${data2.appName || "our platform"}.</p>
|
|
915
|
+
<p>Get started by verifying your email.</p>
|
|
916
|
+
`,
|
|
917
|
+
passwordReset: (data2) => `
|
|
918
|
+
<h1>Password Reset</h1>
|
|
919
|
+
<p>Click <a href="${data2.resetUrl}">here</a> to reset your password.</p>
|
|
920
|
+
<p>This link expires in ${data2.expiry || "1 hour"}.</p>
|
|
921
|
+
`,
|
|
922
|
+
verification: (data2) => `
|
|
923
|
+
<h1>Verify Your Email</h1>
|
|
924
|
+
<p>Click <a href="${data2.verifyUrl}">here</a> to verify your email.</p>
|
|
925
|
+
`
|
|
926
|
+
};
|
|
927
|
+
const template = templates[templateName];
|
|
928
|
+
return template ? template(data) : void 0;
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
var SMSService = class {
|
|
932
|
+
accountSid;
|
|
933
|
+
authToken;
|
|
934
|
+
phoneNumber;
|
|
935
|
+
initialized = false;
|
|
936
|
+
constructor() {
|
|
937
|
+
this.accountSid = config.get("TWILIO_ACCOUNT_SID") || "";
|
|
938
|
+
this.authToken = config.get("TWILIO_AUTH_TOKEN") || "";
|
|
939
|
+
this.phoneNumber = config.get("TWILIO_PHONE_NUMBER") || "";
|
|
940
|
+
this.initialized = !!(this.accountSid && this.authToken && this.phoneNumber);
|
|
941
|
+
}
|
|
942
|
+
async send(options) {
|
|
943
|
+
if (!this.initialized) {
|
|
944
|
+
logger.warn("Twilio not configured, skipping SMS");
|
|
945
|
+
return { sid: "mock-sid" };
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
const twilio = await import('twilio');
|
|
949
|
+
const client = twilio.default(this.accountSid, this.authToken);
|
|
950
|
+
const result = await client.messages.create({
|
|
951
|
+
body: options.message,
|
|
952
|
+
from: options.from || this.phoneNumber,
|
|
953
|
+
to: options.to
|
|
954
|
+
});
|
|
955
|
+
logger.info(`SMS sent to ${options.to}`, { sid: result.sid });
|
|
956
|
+
return { sid: result.sid };
|
|
957
|
+
} catch (error) {
|
|
958
|
+
logger.error("Failed to send SMS", { error });
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
var WebhookService = class {
|
|
964
|
+
async send(options) {
|
|
965
|
+
const response2 = await fetch(options.url, {
|
|
966
|
+
method: options.method || "POST",
|
|
967
|
+
headers: {
|
|
968
|
+
"Content-Type": "application/json",
|
|
969
|
+
...options.headers
|
|
970
|
+
},
|
|
971
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
972
|
+
signal: options.timeout ? AbortSignal.timeout(options.timeout) : void 0
|
|
973
|
+
});
|
|
974
|
+
const body = await response2.json().catch(() => null);
|
|
975
|
+
logger.info(`Webhook sent to ${options.url}`, { status: response2.status });
|
|
976
|
+
return { status: response2.status, body };
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
var SlackService = class {
|
|
980
|
+
webhookUrl;
|
|
981
|
+
constructor() {
|
|
982
|
+
this.webhookUrl = config.get("SLACK_WEBHOOK_URL") || "";
|
|
983
|
+
}
|
|
984
|
+
setWebhookUrl(url) {
|
|
985
|
+
this.webhookUrl = url;
|
|
986
|
+
}
|
|
987
|
+
async send(options) {
|
|
988
|
+
if (!this.webhookUrl) {
|
|
989
|
+
logger.warn("Slack webhook not configured, skipping message");
|
|
990
|
+
return { ok: false };
|
|
991
|
+
}
|
|
992
|
+
const payload = {
|
|
993
|
+
text: options.text,
|
|
994
|
+
blocks: options.blocks,
|
|
995
|
+
channel: options.channel,
|
|
996
|
+
username: options.username,
|
|
997
|
+
icon_emoji: options.iconEmoji
|
|
998
|
+
};
|
|
999
|
+
const response2 = await fetch(this.webhookUrl, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
headers: { "Content-Type": "application/json" },
|
|
1002
|
+
body: JSON.stringify(payload)
|
|
1003
|
+
});
|
|
1004
|
+
const ok = response2.ok;
|
|
1005
|
+
if (ok) {
|
|
1006
|
+
logger.info("Slack message sent");
|
|
1007
|
+
} else {
|
|
1008
|
+
logger.error("Failed to send Slack message");
|
|
1009
|
+
}
|
|
1010
|
+
return { ok };
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
var NotificationService = class {
|
|
1014
|
+
email;
|
|
1015
|
+
sms;
|
|
1016
|
+
webhook;
|
|
1017
|
+
slack;
|
|
1018
|
+
constructor() {
|
|
1019
|
+
this.email = new EmailService();
|
|
1020
|
+
this.sms = new SMSService();
|
|
1021
|
+
this.webhook = new WebhookService();
|
|
1022
|
+
this.slack = new SlackService();
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var notify = new NotificationService();
|
|
1026
|
+
var notification = {
|
|
1027
|
+
email: (options) => notify.email.send(options),
|
|
1028
|
+
sms: (options) => notify.sms.send(options),
|
|
1029
|
+
webhook: (options) => notify.webhook.send(options),
|
|
1030
|
+
slack: (options) => notify.slack.send(options)
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// src/index.ts
|
|
1034
|
+
init_logger();
|
|
1035
|
+
|
|
1036
|
+
// src/rate-limit/index.ts
|
|
1037
|
+
init_express();
|
|
1038
|
+
|
|
1039
|
+
// src/index.ts
|
|
1040
|
+
init_config();
|
|
1041
|
+
var ResponseHelper = class {
|
|
1042
|
+
static success(res, data, message, statusCode = 200) {
|
|
1043
|
+
const response2 = {
|
|
1044
|
+
success: true,
|
|
1045
|
+
data,
|
|
1046
|
+
message
|
|
1047
|
+
};
|
|
1048
|
+
return res.status(statusCode).json(response2);
|
|
1049
|
+
}
|
|
1050
|
+
static created(res, data, message = "Resource created") {
|
|
1051
|
+
return this.success(res, data, message, 201);
|
|
1052
|
+
}
|
|
1053
|
+
static updated(res, data, message = "Resource updated") {
|
|
1054
|
+
return this.success(res, data, message, 200);
|
|
1055
|
+
}
|
|
1056
|
+
static deleted(res, message = "Resource deleted") {
|
|
1057
|
+
return this.success(res, null, message, 200);
|
|
1058
|
+
}
|
|
1059
|
+
static error(res, error, statusCode = 400, code, details) {
|
|
1060
|
+
const response2 = {
|
|
1061
|
+
success: false,
|
|
1062
|
+
error,
|
|
1063
|
+
code,
|
|
1064
|
+
...details && { details }
|
|
1065
|
+
};
|
|
1066
|
+
return res.status(statusCode).json(response2);
|
|
1067
|
+
}
|
|
1068
|
+
static badRequest(res, error = "Bad request", code) {
|
|
1069
|
+
return this.error(res, error, 400, code);
|
|
1070
|
+
}
|
|
1071
|
+
static unauthorized(res, error = "Unauthorized", code) {
|
|
1072
|
+
return this.error(res, error, 401, code);
|
|
1073
|
+
}
|
|
1074
|
+
static forbidden(res, error = "Forbidden", code) {
|
|
1075
|
+
return this.error(res, error, 403, code);
|
|
1076
|
+
}
|
|
1077
|
+
static notFound(res, error = "Resource not found", code) {
|
|
1078
|
+
return this.error(res, error, 404, code);
|
|
1079
|
+
}
|
|
1080
|
+
static conflict(res, error = "Conflict", code) {
|
|
1081
|
+
return this.error(res, error, 409, code);
|
|
1082
|
+
}
|
|
1083
|
+
static validationError(res, error, details) {
|
|
1084
|
+
return this.error(res, error, 422, "VALIDATION_ERROR", details);
|
|
1085
|
+
}
|
|
1086
|
+
static internalError(res, error = "Internal server error") {
|
|
1087
|
+
return this.error(res, error, 500, "INTERNAL_ERROR");
|
|
1088
|
+
}
|
|
1089
|
+
static paginated(res, data, page, limit, total) {
|
|
1090
|
+
const totalPages = Math.ceil(total / limit);
|
|
1091
|
+
const response2 = {
|
|
1092
|
+
success: true,
|
|
1093
|
+
data,
|
|
1094
|
+
meta: {
|
|
1095
|
+
page,
|
|
1096
|
+
limit,
|
|
1097
|
+
total,
|
|
1098
|
+
totalPages
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
return res.status(200).json(response2);
|
|
1102
|
+
}
|
|
1103
|
+
static noContent(res) {
|
|
1104
|
+
return res.status(204).send();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
Response.prototype.success = function(data, message, statusCode = 200) {
|
|
1108
|
+
return ResponseHelper.success(this, data, message, statusCode);
|
|
1109
|
+
};
|
|
1110
|
+
Response.prototype.created = function(data, message) {
|
|
1111
|
+
return ResponseHelper.created(this, data, message);
|
|
1112
|
+
};
|
|
1113
|
+
Response.prototype.updated = function(data, message) {
|
|
1114
|
+
return ResponseHelper.updated(this, data, message);
|
|
1115
|
+
};
|
|
1116
|
+
Response.prototype.deleted = function(message) {
|
|
1117
|
+
return ResponseHelper.deleted(this, message);
|
|
1118
|
+
};
|
|
1119
|
+
Response.prototype.error = function(error, statusCode = 400, code, details) {
|
|
1120
|
+
return ResponseHelper.error(this, error, statusCode, code, details);
|
|
1121
|
+
};
|
|
1122
|
+
Response.prototype.badRequest = function(error, code) {
|
|
1123
|
+
return ResponseHelper.badRequest(this, error, code);
|
|
1124
|
+
};
|
|
1125
|
+
Response.prototype.unauthorized = function(error, code) {
|
|
1126
|
+
return ResponseHelper.unauthorized(this, error, code);
|
|
1127
|
+
};
|
|
1128
|
+
Response.prototype.forbidden = function(error, code) {
|
|
1129
|
+
return ResponseHelper.forbidden(this, error, code);
|
|
1130
|
+
};
|
|
1131
|
+
Response.prototype.notFound = function(error, code) {
|
|
1132
|
+
return ResponseHelper.notFound(this, error, code);
|
|
1133
|
+
};
|
|
1134
|
+
Response.prototype.conflict = function(error, code) {
|
|
1135
|
+
return ResponseHelper.conflict(this, error, code);
|
|
1136
|
+
};
|
|
1137
|
+
Response.prototype.validationError = function(error, details) {
|
|
1138
|
+
return ResponseHelper.validationError(this, error, details);
|
|
1139
|
+
};
|
|
1140
|
+
Response.prototype.internalError = function(error) {
|
|
1141
|
+
return ResponseHelper.internalError(this, error);
|
|
1142
|
+
};
|
|
1143
|
+
Response.prototype.paginated = function(data, page, limit, total) {
|
|
1144
|
+
return ResponseHelper.paginated(this, data, page, limit, total);
|
|
1145
|
+
};
|
|
1146
|
+
var response = ResponseHelper;
|
|
1147
|
+
|
|
1148
|
+
// src/plugin.ts
|
|
1149
|
+
init_logger();
|
|
1150
|
+
init_config();
|
|
1151
|
+
var PluginManager = class {
|
|
1152
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1153
|
+
app = null;
|
|
1154
|
+
authService = null;
|
|
1155
|
+
queueManager = null;
|
|
1156
|
+
register(plugin) {
|
|
1157
|
+
this.plugins.set(plugin.name, plugin);
|
|
1158
|
+
logger.info(`Plugin "${plugin.name}" registered`);
|
|
1159
|
+
}
|
|
1160
|
+
unregister(name) {
|
|
1161
|
+
this.plugins.delete(name);
|
|
1162
|
+
logger.info(`Plugin "${name}" unregistered`);
|
|
1163
|
+
}
|
|
1164
|
+
get(name) {
|
|
1165
|
+
return this.plugins.get(name);
|
|
1166
|
+
}
|
|
1167
|
+
getAll() {
|
|
1168
|
+
return Array.from(this.plugins.values());
|
|
1169
|
+
}
|
|
1170
|
+
has(name) {
|
|
1171
|
+
return this.plugins.has(name);
|
|
1172
|
+
}
|
|
1173
|
+
async initializeAll(app) {
|
|
1174
|
+
this.app = app;
|
|
1175
|
+
for (const plugin of this.plugins.values()) {
|
|
1176
|
+
logger.info(`Initializing plugin "${plugin.name}"`);
|
|
1177
|
+
await plugin.initialize(app);
|
|
1178
|
+
if (plugin.middleware && "use" in app) {
|
|
1179
|
+
for (const mw of plugin.middleware) {
|
|
1180
|
+
app.use(mw);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
var SaaSAppBuilder = class {
|
|
1187
|
+
options;
|
|
1188
|
+
pluginManager;
|
|
1189
|
+
initialized = false;
|
|
1190
|
+
constructor(options = {}) {
|
|
1191
|
+
this.options = options;
|
|
1192
|
+
this.pluginManager = new PluginManager();
|
|
1193
|
+
}
|
|
1194
|
+
use(plugin) {
|
|
1195
|
+
this.pluginManager.register(plugin);
|
|
1196
|
+
return this;
|
|
1197
|
+
}
|
|
1198
|
+
async initialize(app) {
|
|
1199
|
+
if (this.initialized) {
|
|
1200
|
+
throw new Error("App already initialized");
|
|
1201
|
+
}
|
|
1202
|
+
config.load();
|
|
1203
|
+
if (this.options.logger !== false) {
|
|
1204
|
+
const loggerOptions = typeof this.options.logger === "object" ? this.options.logger : {};
|
|
1205
|
+
logger.create(loggerOptions);
|
|
1206
|
+
}
|
|
1207
|
+
if (this.options.auth !== false) {
|
|
1208
|
+
const authOptions = typeof this.options.auth === "object" ? this.options.auth : {};
|
|
1209
|
+
this.authService = createAuth(authOptions);
|
|
1210
|
+
await this.authService.initialize();
|
|
1211
|
+
this.pluginManager.register({
|
|
1212
|
+
name: "auth",
|
|
1213
|
+
initialize: (app2) => {
|
|
1214
|
+
if ("use" in app2) {
|
|
1215
|
+
app2.use(this.authService.getMiddleware());
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
middleware: [this.authService.getMiddleware()]
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
if (this.options.queue !== false) {
|
|
1222
|
+
const queueOptions = typeof this.options.queue === "object" ? this.options.queue : {};
|
|
1223
|
+
if (queueOptions.redisUrl) {
|
|
1224
|
+
(await Promise.resolve().then(() => (init_queue(), queue_exports))).queue.setRedisOptions({ url: queueOptions.redisUrl });
|
|
1225
|
+
}
|
|
1226
|
+
this.pluginManager.register({
|
|
1227
|
+
name: "queue",
|
|
1228
|
+
initialize: () => {
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
if (this.options.rateLimit !== false) {
|
|
1233
|
+
const rateLimitOptions = typeof this.options.rateLimit === "object" ? this.options.rateLimit : {};
|
|
1234
|
+
this.pluginManager.register({
|
|
1235
|
+
name: "rate-limit",
|
|
1236
|
+
initialize: (app2) => {
|
|
1237
|
+
if ("use" in app2) {
|
|
1238
|
+
app2.use(rateLimit(rateLimitOptions));
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
middleware: [rateLimit(rateLimitOptions)]
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (this.options.notifications !== false) {
|
|
1245
|
+
this.pluginManager.register({
|
|
1246
|
+
name: "notifications",
|
|
1247
|
+
initialize: () => {
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
await this.pluginManager.initializeAll(app);
|
|
1252
|
+
this.initialized = true;
|
|
1253
|
+
logger.info("SaaS App initialized");
|
|
1254
|
+
}
|
|
1255
|
+
getAuth() {
|
|
1256
|
+
return this.authService;
|
|
1257
|
+
}
|
|
1258
|
+
getPluginManager() {
|
|
1259
|
+
return this.pluginManager;
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
function createApp(options = {}) {
|
|
1263
|
+
return new SaaSAppBuilder(options);
|
|
1264
|
+
}
|
|
1265
|
+
function createExpressApp(options = {}) {
|
|
1266
|
+
const express = __require("express");
|
|
1267
|
+
const app = express();
|
|
1268
|
+
const builder = createApp({ ...options, framework: "express" });
|
|
1269
|
+
builder.initialize(app);
|
|
1270
|
+
return app;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/index.ts
|
|
1274
|
+
init_types();
|
|
1275
|
+
init_queue();
|
|
1276
|
+
init_logger();
|
|
1277
|
+
init_config();
|
|
1278
|
+
|
|
1279
|
+
export { Auth, AuthService, PluginManager, QueueManager, ResponseHelper, SaaSAppBuilder, auth, config, createApp, createAuth, createExpressApp, createQueue, createRateLimiter, logger, notification, notify, queue, rateLimit, response };
|
|
1280
|
+
//# sourceMappingURL=index.mjs.map
|
|
1281
|
+
//# sourceMappingURL=index.mjs.map
|