aaspai-authx 0.0.1

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.
@@ -0,0 +1,1699 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/express/index.ts
31
+ var express_exports = {};
32
+ __export(express_exports, {
33
+ createAdminRouter: () => createAdminRouter,
34
+ createAuthRouter: () => createAuthRouter,
35
+ createDashboardRouter: () => createDashboardRouter,
36
+ createEmailRouter: () => createEmailRouter,
37
+ createProjectsRouter: () => createProjectsRouter
38
+ });
39
+ module.exports = __toCommonJS(express_exports);
40
+
41
+ // src/express/auth.routes.ts
42
+ var import_bcryptjs = __toESM(require("bcryptjs"), 1);
43
+ var import_crypto = require("crypto");
44
+ var import_express = __toESM(require("express"), 1);
45
+ var import_jsonwebtoken4 = __toESM(require("jsonwebtoken"), 1);
46
+
47
+ // src/config/loadConfig.ts
48
+ function loadConfig() {
49
+ return {
50
+ orgDomain: process.env.ORG_DOMAIN,
51
+ orgId: process.env.ORG_ID,
52
+ email: {
53
+ host: process.env.EMAIL_HOST || "smtp.postmarkapp.com",
54
+ port: process.env.EMAIL_PORT ? Number(process.env.EMAIL_PORT) : 587,
55
+ secure: (process.env.EMAIL_SECURE || "false") === "true",
56
+ user: process.env.EMAIL_USER,
57
+ pass: process.env.EMAIL_PASSWORD,
58
+ from: process.env.EMAIL_FROM,
59
+ jwtSecret: process.env.EMAIL_JWT_SECRET
60
+ },
61
+ cookies: {
62
+ domain: process.env.COOKIE_DOMAIN,
63
+ secure: (process.env.COOKIE_SECURE || "true") === "true",
64
+ accessTtlMs: 24 * 60 * 60 * 1e3,
65
+ refreshTtlMs: 7 * 24 * 60 * 60 * 1e3
66
+ },
67
+ oidc: {
68
+ jwtSecret: process.env.JWT_SECRET
69
+ },
70
+ aws: {
71
+ bucket: process.env.AWS_S3_BUCKET,
72
+ region: process.env.AWS_REGION,
73
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
74
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
75
+ }
76
+ };
77
+ }
78
+
79
+ // src/config/index.ts
80
+ var config = loadConfig();
81
+ function configureAuthX(overrides = {}) {
82
+ return deepMerge(config, overrides);
83
+ }
84
+ function deepMerge(target, source) {
85
+ if (!source) {
86
+ return target;
87
+ }
88
+ for (const key of Object.keys(source)) {
89
+ const value = source[key];
90
+ if (value === void 0) continue;
91
+ if (Array.isArray(value)) {
92
+ target[key] = [...value];
93
+ continue;
94
+ }
95
+ if (isPlainObject(value)) {
96
+ if (!isPlainObject(target[key])) {
97
+ target[key] = {};
98
+ }
99
+ deepMerge(target[key], value);
100
+ continue;
101
+ }
102
+ target[key] = value;
103
+ }
104
+ return target;
105
+ }
106
+ function isPlainObject(value) {
107
+ return typeof value === "object" && value !== null && !Array.isArray(value);
108
+ }
109
+
110
+ // src/core/roles.config.ts
111
+ var PLATFORM_ROLES = [
112
+ {
113
+ role: "platform_admin",
114
+ permissions: [
115
+ "projects.create",
116
+ "projects.read",
117
+ "projects.update",
118
+ "projects.delete",
119
+ "users.manage",
120
+ "api.manage"
121
+ ]
122
+ },
123
+ {
124
+ role: "platform_manager",
125
+ permissions: [
126
+ "projects.read",
127
+ "projects.update",
128
+ "users.read"
129
+ ]
130
+ },
131
+ {
132
+ role: "platform_user",
133
+ permissions: ["projects.read"]
134
+ }
135
+ ];
136
+ function getPermissionsForRoles(roles) {
137
+ if (!Array.isArray(roles) || roles.length === 0) {
138
+ return [];
139
+ }
140
+ const permissionSet = /* @__PURE__ */ new Set();
141
+ for (const roleName of roles) {
142
+ const roleConfig = PLATFORM_ROLES.find((r) => r.role === roleName);
143
+ if (roleConfig && Array.isArray(roleConfig.permissions)) {
144
+ for (const perm of roleConfig.permissions) {
145
+ permissionSet.add(perm);
146
+ }
147
+ }
148
+ }
149
+ return Array.from(permissionSet);
150
+ }
151
+
152
+ // src/core/session.ts
153
+ function buildSession(payload) {
154
+ const userId = payload?.sub || payload?.userId || payload?.id || "";
155
+ const email = payload?.email || payload?.email_address || "";
156
+ const roles = payload?.realm_access?.roles || payload?.roles || payload?.["cognito:groups"] || (Array.isArray(payload?.role) ? payload.role : []) || [];
157
+ const normalizedRoles = Array.isArray(roles) ? roles.map(String).filter(Boolean) : [];
158
+ const permissions = getPermissionsForRoles(normalizedRoles);
159
+ const session = {
160
+ userId,
161
+ email,
162
+ roles: normalizedRoles,
163
+ permissions
164
+ };
165
+ if (payload?.projectId) session.projectId = payload.projectId;
166
+ if (payload?.orgId) session.orgId = payload.orgId;
167
+ if (payload?.org_id) session.org_id = payload.org_id;
168
+ if (payload?.authType) session.authType = payload.authType;
169
+ Object.keys(payload || {}).forEach((key) => {
170
+ if (![
171
+ "sub",
172
+ "userId",
173
+ "id",
174
+ "email",
175
+ "email_address",
176
+ "realm_access",
177
+ "roles",
178
+ "cognito:groups",
179
+ "role",
180
+ "projectId",
181
+ "orgId",
182
+ "org_id",
183
+ "authType"
184
+ ].includes(key)) {
185
+ session[key] = payload[key];
186
+ }
187
+ });
188
+ return session;
189
+ }
190
+
191
+ // src/models/user.model.ts
192
+ var import_mongoose = __toESM(require("mongoose"), 1);
193
+ var import_uuid = require("uuid");
194
+ var MetadataSchema = new import_mongoose.default.Schema(
195
+ {
196
+ key: { type: String, required: true },
197
+ value: { type: import_mongoose.default.Schema.Types.Mixed, required: true }
198
+ },
199
+ { _id: false }
200
+ );
201
+ var OrgUserSchema = new import_mongoose.default.Schema(
202
+ {
203
+ id: { type: String, default: (0, import_uuid.v4)(), index: true },
204
+ email: { type: String, required: true, unique: true },
205
+ firstName: { type: String, required: true },
206
+ lastName: { type: String, required: true },
207
+ orgId: { type: String },
208
+ projectId: { type: String, required: true },
209
+ roles: { type: [String], default: [] },
210
+ emailVerified: { type: Boolean, default: false },
211
+ lastEmailSent: { type: [Date], default: [] },
212
+ lastPasswordReset: { type: Date },
213
+ metadata: { type: [MetadataSchema], default: [] },
214
+ passwordHash: { type: String }
215
+ },
216
+ { timestamps: true, collection: "users" }
217
+ );
218
+ var OrgUser = import_mongoose.default.model("OrgUser", OrgUserSchema);
219
+
220
+ // src/utils/extract.ts
221
+ var import_cookie = require("cookie");
222
+ function extractToken(req, opts) {
223
+ const headerNames = opts?.headerNames ?? ["authorization", "token"];
224
+ const cookieNames = opts?.cookieNames ?? ["access_token", "authorization"];
225
+ const queryNames = opts?.queryNames ?? ["access_token", "token"];
226
+ for (const h of headerNames) {
227
+ const raw = req.headers[h];
228
+ if (raw) {
229
+ const lower = raw.toLowerCase();
230
+ if (lower.startsWith("bearer ")) return raw.slice(7).trim();
231
+ if (!raw.includes(" ")) return raw.trim();
232
+ }
233
+ }
234
+ const ch = req.headers["cookie"];
235
+ if (typeof ch === "string") {
236
+ const parsed = (0, import_cookie.parse)(ch);
237
+ for (const c of cookieNames) if (parsed[c]) return parsed[c];
238
+ }
239
+ for (const q of queryNames) {
240
+ const v = req.query?.[q];
241
+ if (typeof v === "string" && v) return v;
242
+ }
243
+ return null;
244
+ }
245
+ function readProjectId(req) {
246
+ const ch = req.headers["cookie"];
247
+ if (typeof ch === "string") {
248
+ try {
249
+ const parsed = (0, import_cookie.parse)(ch);
250
+ return parsed["projectId"] || null;
251
+ } catch {
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+
257
+ // src/utils/jwt.ts
258
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
259
+ function verifyJwt(token) {
260
+ return new Promise((resolve, reject) => {
261
+ import_jsonwebtoken.default.verify(
262
+ token,
263
+ process.env.JWT_SECRET,
264
+ // This is your shared secret (string)
265
+ {
266
+ algorithms: ["HS256"],
267
+ // Only allow HS256
268
+ complete: false
269
+ // We only want payload
270
+ },
271
+ (err, decoded) => {
272
+ if (err) {
273
+ reject(err);
274
+ } else {
275
+ resolve(decoded);
276
+ }
277
+ }
278
+ );
279
+ });
280
+ }
281
+
282
+ // src/middlewares/auth.middleware.ts
283
+ function requireAuth() {
284
+ return async (req, res, next) => {
285
+ try {
286
+ const apiKey = req.headers["x-api-key"] || req.headers["x-apikey"];
287
+ const userId = req.headers["x-user-id"] || req.headers["x-userId"];
288
+ if (apiKey) {
289
+ if (apiKey !== process.env.SERVER_API_KEY) {
290
+ return res.status(401).json({ error: "Invalid API key" });
291
+ }
292
+ if (!userId) {
293
+ return res.status(401).json({ error: "User Id is Required" });
294
+ }
295
+ const user = await OrgUser.findOne({ id: userId }).lean();
296
+ if (!user) {
297
+ return res.status(401).json({ error: "User not found" });
298
+ }
299
+ const session2 = buildSession({
300
+ sub: user.id.toString(),
301
+ email: user.email,
302
+ roles: user.roles || []
303
+ });
304
+ session2.authType = "api-key";
305
+ session2.projectId = readProjectId(req) || user.projectId || void 0;
306
+ req.user = session2;
307
+ return next();
308
+ }
309
+ const token = extractToken(req);
310
+ if (!token) {
311
+ return res.status(401).json({ error: "Missing token" });
312
+ }
313
+ const claims = await verifyJwt(token);
314
+ const session = buildSession(claims);
315
+ const pid = readProjectId(req);
316
+ if (pid) session.projectId = pid;
317
+ req.user = session;
318
+ next();
319
+ } catch (e) {
320
+ res.status(401).json({ error: e?.message || "Unauthorized" });
321
+ }
322
+ };
323
+ }
324
+
325
+ // src/middlewares/validators.ts
326
+ function isEmail(v) {
327
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
328
+ }
329
+ function isPasswordStrong(v) {
330
+ return typeof v === "string" && v.length >= 6 && /[A-Z]/.test(v) && /[^a-zA-Z0-9]/.test(v);
331
+ }
332
+ function validateSignup(req, res, next) {
333
+ const { firstName, lastName, email, password, projectId, metadata } = req.body || {};
334
+ if (!firstName || !lastName)
335
+ return res.status(400).json({ error: "firstName,lastName required" });
336
+ if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
337
+ if (!isPasswordStrong(password))
338
+ return res.status(400).json({ error: "weak password" });
339
+ if (!projectId) return res.status(400).json({ error: "projectId required" });
340
+ if (!Array.isArray(metadata))
341
+ return res.status(400).json({ error: "metadata must be array" });
342
+ next();
343
+ }
344
+ function validateLogin(req, res, next) {
345
+ const { email, password } = req.body || {};
346
+ if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
347
+ if (typeof password !== "string")
348
+ return res.status(400).json({ error: "password required" });
349
+ next();
350
+ }
351
+ function validateResetPassword(req, res, next) {
352
+ const { token, newPassword } = req.body || {};
353
+ if (!token) return res.status(400).json({ error: "token required" });
354
+ if (!isPasswordStrong(newPassword))
355
+ return res.status(400).json({ error: "weak password" });
356
+ next();
357
+ }
358
+ function validateResendEmail(req, res, next) {
359
+ const { email } = req.body || {};
360
+ if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
361
+ next();
362
+ }
363
+ function validateSendInvite(req, res, next) {
364
+ const { email, role } = req.body || {};
365
+ if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
366
+ if (!["platform_user", "org_admin"].includes(role))
367
+ return res.status(400).json({ error: "invalid role" });
368
+ next();
369
+ }
370
+
371
+ // src/models/invite.model.ts
372
+ var import_mongoose2 = __toESM(require("mongoose"), 1);
373
+ var InviteSchema = new import_mongoose2.default.Schema(
374
+ {
375
+ id: { type: String, required: true, index: true },
376
+ email: { type: String, required: true },
377
+ role: {
378
+ type: String,
379
+ enum: ["platform_user", "org_admin"],
380
+ required: true
381
+ },
382
+ invitedBy: { type: String },
383
+ usedBy: { type: String },
384
+ isUsed: { type: Boolean, default: false },
385
+ usedAt: { type: Date },
386
+ expiresAt: { type: Date },
387
+ isExpired: { type: Boolean, default: false }
388
+ },
389
+ { timestamps: true, collection: "invites" }
390
+ );
391
+ var Invite = import_mongoose2.default.model("Invite", InviteSchema);
392
+
393
+ // src/services/auth-admin.service.ts
394
+ var import_bcrypt = __toESM(require("bcrypt"), 1);
395
+ var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
396
+
397
+ // src/models/client.model.ts
398
+ var import_mongoose3 = __toESM(require("mongoose"), 1);
399
+ var ClientSchema = new import_mongoose3.Schema(
400
+ {
401
+ clientId: {
402
+ type: String,
403
+ required: true,
404
+ unique: true,
405
+ index: true
406
+ },
407
+ redirectUris: {
408
+ type: [String],
409
+ default: []
410
+ },
411
+ publicClient: {
412
+ type: Boolean,
413
+ default: false
414
+ },
415
+ // Optional: if you want confidential clients
416
+ secret: {
417
+ type: String,
418
+ required: function() {
419
+ return !this.publicClient;
420
+ }
421
+ }
422
+ },
423
+ {
424
+ timestamps: true
425
+ }
426
+ );
427
+ var ClientModel = import_mongoose3.default.models.Client || import_mongoose3.default.model("Client", ClientSchema);
428
+
429
+ // src/models/rolePermission.model.ts
430
+ var import_mongoose4 = __toESM(require("mongoose"), 1);
431
+ var RolePermissionSchema = new import_mongoose4.Schema(
432
+ {
433
+ orgId: { type: String, default: null, index: true },
434
+ role: { type: String, required: true },
435
+ permissions: { type: [String], default: [] }
436
+ },
437
+ {
438
+ timestamps: true
439
+ }
440
+ );
441
+ RolePermissionSchema.index({ orgId: 1, role: 1 }, { unique: true });
442
+ var RolePermissionModel = import_mongoose4.default.model(
443
+ "RolePermission",
444
+ RolePermissionSchema,
445
+ "role_permissions"
446
+ );
447
+
448
+ // src/services/auth-admin.service.ts
449
+ var AuthAdminService = class {
450
+ token;
451
+ async getAdminToken() {
452
+ return this.ensureAdminToken();
453
+ }
454
+ // -------------------------------------------------------------------
455
+ // CLIENTS
456
+ // -------------------------------------------------------------------
457
+ async createClient(clientId, redirectUris = [], publicClient = false) {
458
+ const client = await ClientModel.create({
459
+ clientId,
460
+ redirectUris,
461
+ publicClient
462
+ });
463
+ return client;
464
+ }
465
+ async updateClient(id, patch) {
466
+ await ClientModel.findByIdAndUpdate(id, patch);
467
+ }
468
+ // -------------------------------------------------------------------
469
+ // USERS
470
+ // -------------------------------------------------------------------
471
+ async listUsersInRealm(_realm, filter) {
472
+ return OrgUser.find(filter || {});
473
+ }
474
+ async getUserById(userId) {
475
+ return OrgUser.findOne({ id: userId });
476
+ }
477
+ async isUserEmailVerified(userId) {
478
+ const user = await OrgUser.findOne({ id: userId });
479
+ return user?.emailVerified;
480
+ }
481
+ async createUserInRealm(payload) {
482
+ const hashedPassword = payload.credentials?.[0]?.value ? await import_bcrypt.default.hash(payload.credentials[0].value, 10) : void 0;
483
+ const user = await OrgUser.create({
484
+ username: payload.username,
485
+ email: payload.email,
486
+ firstName: payload.firstName,
487
+ lastName: payload.lastName,
488
+ projectId: payload.projectId,
489
+ emailVerified: payload.emailVerified || false,
490
+ passwordHash: hashedPassword,
491
+ enabled: true
492
+ });
493
+ return user;
494
+ }
495
+ async assignRealmRole(userId, roleName) {
496
+ const role = await RolePermissionModel.findOne({ name: roleName });
497
+ if (!role) throw new Error(`Role not found: ${roleName}`);
498
+ await OrgUser.findOneAndUpdate(
499
+ { id: userId },
500
+ {
501
+ $addToSet: { roles: role._id }
502
+ }
503
+ );
504
+ }
505
+ async updateUserEmailVerified(userId, emailVerified) {
506
+ await OrgUser.findOneAndUpdate({ id: userId }, { emailVerified });
507
+ }
508
+ async updateUserPassword(userId, newPassword) {
509
+ const hashed = await import_bcrypt.default.hash(newPassword, 10);
510
+ await OrgUser.findOneAndUpdate({ id: userId }, { password: hashed });
511
+ }
512
+ // -------------------------------------------------------------------
513
+ // ADMIN TOKEN (self-issued JWT)
514
+ // -------------------------------------------------------------------
515
+ async ensureAdminToken() {
516
+ const now = Math.floor(Date.now() / 1e3);
517
+ if (this.token && this.token.exp - 30 > now) {
518
+ return this.token.accessToken;
519
+ }
520
+ const payload = {
521
+ type: "admin",
522
+ system: true
523
+ };
524
+ const accessToken = import_jsonwebtoken2.default.sign(payload, process.env.JWT_SECRET, {
525
+ expiresIn: "1h"
526
+ });
527
+ this.token = {
528
+ accessToken,
529
+ exp: now + 3600
530
+ };
531
+ return this.token.accessToken;
532
+ }
533
+ };
534
+
535
+ // src/services/email.service.ts
536
+ var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
537
+ var import_nodemailer = __toESM(require("nodemailer"), 1);
538
+ var EmailService = class {
539
+ transporter;
540
+ MAX_EMAILS = 5;
541
+ WINDOW_MINUTES = 15;
542
+ BLOCK_HOURS = 1;
543
+ constructor() {
544
+ this.transporter = import_nodemailer.default.createTransport({
545
+ host: process.env.EMAIL_HOST || "smtp.postmarkapp.com",
546
+ port: process.env.EMAIL_PORT ? Number(process.env.EMAIL_PORT) : 587,
547
+ secure: (process.env.EMAIL_SECURE || "false") === "true",
548
+ auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD }
549
+ });
550
+ }
551
+ sign(payload, ttlSec = 60 * 60 * 24) {
552
+ return import_jsonwebtoken3.default.sign(payload, process.env.EMAIL_JWT_SECRET, { expiresIn: ttlSec });
553
+ }
554
+ verify(token) {
555
+ return import_jsonwebtoken3.default.verify(token, process.env.EMAIL_JWT_SECRET);
556
+ }
557
+ async send(to, subject, html) {
558
+ await this.transporter.sendMail({
559
+ from: process.env.EMAIL_FROM,
560
+ to,
561
+ subject,
562
+ html
563
+ });
564
+ }
565
+ canSend(lastEmailSent) {
566
+ const now = Date.now();
567
+ const windowStart = now - this.WINDOW_MINUTES * 60 * 1e3;
568
+ const emailsInWindow = (lastEmailSent || []).map((d) => new Date(d)).filter((d) => d.getTime() >= windowStart);
569
+ if (emailsInWindow.length >= this.MAX_EMAILS)
570
+ return {
571
+ ok: false,
572
+ reason: "RATE_LIMIT",
573
+ waitMs: this.BLOCK_HOURS * 60 * 60 * 1e3
574
+ };
575
+ return { ok: true };
576
+ }
577
+ };
578
+
579
+ // src/utils/cookie.ts
580
+ function cookieOpts(isRefresh = false) {
581
+ const maxAge = isRefresh ? config.cookies.refreshTtlMs : config.cookies.accessTtlMs;
582
+ const secure = process.env.NODE_ENV === "production" ? process.env.COOKIE_SECURE ?? true : false;
583
+ return {
584
+ httpOnly: true,
585
+ secure,
586
+ sameSite: "none",
587
+ domain: process.env.COOKIE_DOMAIN,
588
+ maxAge
589
+ };
590
+ }
591
+ function clearOpts() {
592
+ const secure = process.env.NODE_ENV === "production" ? process.env.COOKIE_SECURE ?? true : false;
593
+ return {
594
+ domain: process.env.COOKIE_DOMAIN,
595
+ sameSite: "none",
596
+ secure
597
+ };
598
+ }
599
+
600
+ // src/express/auth.routes.ts
601
+ function createAuthRouter(options = {}) {
602
+ if (options.config) {
603
+ configureAuthX(options.config);
604
+ }
605
+ const r = (0, import_express.Router)();
606
+ const email = new EmailService();
607
+ const authAdmin = new AuthAdminService();
608
+ r.use(import_express.default.json());
609
+ r.use(import_express.default.urlencoded({ extended: true }));
610
+ r.get(
611
+ "/healthz",
612
+ (_req, res) => res.json({ status: "ok", server: "org-server" })
613
+ );
614
+ r.post("/login", validateLogin, async (req, res) => {
615
+ const { email: emailAddress, password } = req.body || {};
616
+ try {
617
+ const user = await OrgUser.findOne({ email: emailAddress }).select("+password").lean();
618
+ if (!user) {
619
+ return res.status(400).json({
620
+ error: "Invalid email or password",
621
+ code: "INVALID_CREDENTIALS"
622
+ });
623
+ }
624
+ if (!user.emailVerified) {
625
+ return res.status(400).json({
626
+ error: "Please verify your email before logging in.",
627
+ code: "EMAIL_NOT_VERIFIED"
628
+ });
629
+ }
630
+ const isPasswordValid = user.passwordHash ? await import_bcryptjs.default.compare(password, user.passwordHash) : false;
631
+ if (!isPasswordValid) {
632
+ return res.status(400).json({
633
+ error: "Invalid email or password",
634
+ code: "INVALID_CREDENTIALS"
635
+ });
636
+ }
637
+ const tokens = generateTokens(user);
638
+ setAuthCookies(res, tokens);
639
+ if (user.projectId) {
640
+ res.cookie(options.projectCookieName || "projectId", user.projectId, {
641
+ ...cookieOpts(false),
642
+ httpOnly: true
643
+ });
644
+ }
645
+ return res.json({
646
+ message: "Login successful",
647
+ user: toUserResponse(user)
648
+ });
649
+ } catch (err) {
650
+ console.error("Login error:", err);
651
+ return res.status(500).json({ error: "Internal server error" });
652
+ }
653
+ });
654
+ r.post("/signup", validateSignup, async (req, res) => {
655
+ const {
656
+ firstName,
657
+ lastName,
658
+ email: emailAddress,
659
+ password,
660
+ projectId,
661
+ metadata
662
+ } = req.body || {};
663
+ try {
664
+ const kcUser = await authAdmin.createUserInRealm({
665
+ username: emailAddress,
666
+ email: emailAddress,
667
+ firstName,
668
+ lastName,
669
+ projectId,
670
+ credentials: [{ type: "password", value: password, temporary: false }]
671
+ });
672
+ await authAdmin.assignRealmRole(kcUser.id, "platform_user");
673
+ const user = await OrgUser.findOneAndUpdate(
674
+ { email: kcUser.email },
675
+ {
676
+ id: kcUser.id,
677
+ email: kcUser.email,
678
+ firstName,
679
+ lastName,
680
+ projectId,
681
+ metadata,
682
+ roles: ["platform_user"],
683
+ emailVerified: false
684
+ },
685
+ { upsert: true, new: true, setDefaultsOnInsert: true }
686
+ );
687
+ const emailResult = await sendRateLimitedEmail({
688
+ emailService: email,
689
+ user,
690
+ subject: "Verify your email",
691
+ html: buildVerificationTemplate(
692
+ email.sign({ userId: kcUser.id, email: kcUser.email }),
693
+ options
694
+ )
695
+ });
696
+ if (emailResult.rateLimited) {
697
+ return res.status(429).json({
698
+ ok: false,
699
+ error: "Too many verification emails sent. Please try again later.",
700
+ waitMs: emailResult.waitMs
701
+ });
702
+ }
703
+ return res.json({
704
+ id: user.id,
705
+ email: user.email,
706
+ message: "Verification email sent. Please check your inbox."
707
+ });
708
+ } catch (err) {
709
+ return respondWithKeycloakError(res, err, "Signup failed");
710
+ }
711
+ });
712
+ r.get("/me", requireAuth(), (req, res) => {
713
+ return res.json(req.user || null);
714
+ });
715
+ r.post("/logout", async (_req, res) => {
716
+ res.clearCookie("access_token", clearOpts());
717
+ res.clearCookie("refresh_token", clearOpts());
718
+ res.json({ ok: true });
719
+ });
720
+ r.put("/:userId/metadata", requireAuth(), async (req, res) => {
721
+ const { userId } = req.params;
722
+ const { metadata } = req.body || {};
723
+ const user = await OrgUser.findOne({ id: userId });
724
+ if (!user)
725
+ return res.status(404).json({ ok: false, error: "User not found" });
726
+ const map = new Map(
727
+ (user.metadata || []).map((m) => [m.key, m.value])
728
+ );
729
+ for (const item of metadata || []) map.set(item.key, item.value);
730
+ user.metadata = Array.from(map.entries()).map(([key, value]) => ({
731
+ key,
732
+ value
733
+ }));
734
+ await user.save();
735
+ res.json({ ok: true, metadata: user.metadata });
736
+ });
737
+ r.get("/verify-email", async (req, res) => {
738
+ const token = String(req.query.token || "");
739
+ if (!token) {
740
+ return res.status(400).json({ error: "Verification token is required" });
741
+ }
742
+ try {
743
+ const payload = email.verify(token);
744
+ await authAdmin.updateUserEmailVerified(payload.userId, true);
745
+ await OrgUser.updateOne(
746
+ { id: payload.userId },
747
+ { $set: { emailVerified: true } }
748
+ );
749
+ res.json({ ok: true, message: "Email verified" });
750
+ } catch (err) {
751
+ res.status(400).json({ ok: false, error: err?.message || "Invalid token" });
752
+ }
753
+ });
754
+ r.post(
755
+ "/resend-verification-email",
756
+ validateResendEmail,
757
+ async (req, res) => {
758
+ const user = await OrgUser.findOne({ email: req.body.email });
759
+ if (!user)
760
+ return res.status(404).json({ ok: false, error: "User not found" });
761
+ const verified = await authAdmin.isUserEmailVerified(user.id);
762
+ if (verified) {
763
+ return res.status(400).json({ ok: false, error: "Email is already verified" });
764
+ }
765
+ const token = email.sign({
766
+ email: user.email,
767
+ userId: user.id
768
+ });
769
+ const resendResult = await sendRateLimitedEmail({
770
+ emailService: email,
771
+ user,
772
+ subject: "Verify your email",
773
+ html: buildVerificationTemplate(token, options)
774
+ });
775
+ if (resendResult.rateLimited) {
776
+ return res.status(429).json({
777
+ ok: false,
778
+ error: "Too many verification emails sent. Please try again later.",
779
+ waitMs: resendResult.waitMs
780
+ });
781
+ }
782
+ res.json({ ok: true });
783
+ }
784
+ );
785
+ r.post("/forgot-password", validateResendEmail, async (req, res) => {
786
+ const user = await OrgUser.findOne({ email: req.body.email });
787
+ if (!user)
788
+ return res.status(404).json({ ok: false, error: "User not found" });
789
+ const resetToken = email.sign(
790
+ {
791
+ userId: user.id,
792
+ email: user.email,
793
+ firstName: user.firstName,
794
+ lastName: user.lastName
795
+ },
796
+ 60 * 60
797
+ );
798
+ const resetResult = await sendRateLimitedEmail({
799
+ emailService: email,
800
+ user,
801
+ subject: "Reset password",
802
+ html: buildResetTemplate(resetToken, options)
803
+ });
804
+ if (resetResult.rateLimited) {
805
+ return res.status(429).json({
806
+ ok: false,
807
+ error: "Please wait before requesting another password reset email.",
808
+ waitMs: resetResult.waitMs
809
+ });
810
+ }
811
+ res.json({ ok: true, message: "Password reset email sent" });
812
+ });
813
+ r.post("/reset-password", validateResetPassword, async (req, res) => {
814
+ const { token, newPassword } = req.body || {};
815
+ try {
816
+ const payload = email.verify(token);
817
+ const user = await OrgUser.findOne({ keycloakId: payload.userId });
818
+ if (!user) {
819
+ return res.status(404).json({ ok: false, error: "User not found" });
820
+ }
821
+ if (user.lastPasswordReset && payload.iat * 1e3 < user.lastPasswordReset.getTime()) {
822
+ return res.status(400).json({
823
+ ok: false,
824
+ error: "This reset link has already been used. Please request a new one."
825
+ });
826
+ }
827
+ await authAdmin.updateUserPassword(payload.userId, newPassword);
828
+ user.lastPasswordReset = /* @__PURE__ */ new Date();
829
+ await user.save();
830
+ res.json({ ok: true, message: "Password updated successfully" });
831
+ } catch (err) {
832
+ res.status(400).json({ ok: false, error: err?.message || "Invalid or expired token" });
833
+ }
834
+ });
835
+ r.post(
836
+ "/send-invite",
837
+ requireAuth(),
838
+ validateSendInvite,
839
+ async (req, res) => {
840
+ const { email: emailAddress, role } = req.body || {};
841
+ const existingUser = await OrgUser.findOne({ email: emailAddress });
842
+ if (existingUser) {
843
+ return res.status(400).json({ ok: false, error: "User with this email already exists" });
844
+ }
845
+ const existingInvite = await Invite.findOne({
846
+ email: emailAddress,
847
+ isUsed: false,
848
+ isExpired: false
849
+ });
850
+ if (existingInvite) {
851
+ return res.status(400).json({
852
+ ok: false,
853
+ error: "An active invite already exists for this email"
854
+ });
855
+ }
856
+ const token = email.sign({
857
+ email: emailAddress,
858
+ role,
859
+ inviteId: (0, import_crypto.randomUUID)()
860
+ });
861
+ const invite = await Invite.create({
862
+ id: token,
863
+ email: emailAddress,
864
+ role,
865
+ invitedBy: req.user?.sub,
866
+ isUsed: false,
867
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3)
868
+ });
869
+ await email.send(
870
+ emailAddress,
871
+ "You are invited",
872
+ `<a href="${getFrontendBaseUrl(options)}/auth/accept-invite?token=${token}">Accept</a>`
873
+ );
874
+ res.json({
875
+ ok: true,
876
+ inviteId: invite.id,
877
+ email: invite.email,
878
+ role: invite.role,
879
+ expiresAt: invite.expiresAt
880
+ });
881
+ }
882
+ );
883
+ r.get("/accept-invite", async (req, res) => {
884
+ const inv = await Invite.findOne({ id: String(req.query.token) });
885
+ res.json({ ok: !!inv && !inv.isUsed && !inv.isExpired });
886
+ });
887
+ r.post("/accept-invite", async (req, res) => {
888
+ const { token, firstName, lastName, password, projectId } = req.body || {};
889
+ if (!token || !firstName || !lastName || !isPasswordStrong(password || "")) {
890
+ return res.status(400).json({ ok: false, error: "Invalid payload" });
891
+ }
892
+ const invite = await Invite.findOne({
893
+ id: token,
894
+ isUsed: false,
895
+ isExpired: false
896
+ });
897
+ if (!invite) {
898
+ return res.status(400).json({ ok: false, error: "Invitation not found or already used" });
899
+ }
900
+ if (invite.expiresAt && invite.expiresAt.getTime() < Date.now()) {
901
+ invite.isExpired = true;
902
+ await invite.save();
903
+ return res.status(400).json({ ok: false, error: "Invitation has expired" });
904
+ }
905
+ try {
906
+ const kcUser = await authAdmin.createUserInRealm({
907
+ username: invite.email,
908
+ email: invite.email,
909
+ firstName,
910
+ lastName,
911
+ projectId,
912
+ emailVerified: true,
913
+ credentials: [{ type: "password", value: password, temporary: false }]
914
+ });
915
+ await authAdmin.assignRealmRole(kcUser.id, invite.role);
916
+ await OrgUser.findOneAndUpdate(
917
+ { email: invite.email },
918
+ {
919
+ id: kcUser.id,
920
+ email: invite.email,
921
+ firstName,
922
+ lastName,
923
+ roles: [invite.role],
924
+ emailVerified: true
925
+ },
926
+ { upsert: true, new: true, setDefaultsOnInsert: true }
927
+ );
928
+ invite.isUsed = true;
929
+ invite.usedAt = /* @__PURE__ */ new Date();
930
+ invite.usedBy = kcUser.id;
931
+ await invite.save();
932
+ res.json({
933
+ ok: true,
934
+ message: "Account created successfully.",
935
+ email: invite.email
936
+ });
937
+ } catch (err) {
938
+ res.status(400).json({
939
+ ok: false,
940
+ error: err?.response?.data?.error_description || err?.message || "Failed to create account"
941
+ });
942
+ }
943
+ });
944
+ r.get("/invites", requireAuth(), async (_req, res) => {
945
+ const invites = await Invite.find().sort({ createdAt: -1 }).lean();
946
+ res.json(invites);
947
+ });
948
+ r.delete("/invites/:inviteId", requireAuth(), async (req, res) => {
949
+ await Invite.deleteOne({ id: req.params.inviteId });
950
+ res.json({ ok: true });
951
+ });
952
+ r.get("/get-user-by-email", async (req, res) => {
953
+ const user = await OrgUser.findOne({ email: req.query.email }).lean();
954
+ res.json(user || null);
955
+ });
956
+ r.get("/google", async (_req, res) => {
957
+ res.json({ url: "/auth/google/callback?code=demo" });
958
+ });
959
+ r.get("/google/callback", async (_req, res) => {
960
+ res.cookie(
961
+ "access_token",
962
+ "ACCESS.TOKEN.PLACEHOLDER",
963
+ cookieOpts(false)
964
+ );
965
+ res.redirect("/");
966
+ });
967
+ r.get("/get-users", async (req, res) => {
968
+ const user = await OrgUser.find({ projectId: req.query.projectId }).lean();
969
+ res.json(user || null);
970
+ });
971
+ return r;
972
+ }
973
+ function setAuthCookies(res, tokens) {
974
+ if (tokens?.access_token) {
975
+ res.cookie("access_token", tokens.access_token, {
976
+ httpOnly: true,
977
+ secure: false,
978
+ sameSite: "lax",
979
+ maxAge: 24 * 60 * 60 * 1e3,
980
+ // 24 hours
981
+ path: "/"
982
+ });
983
+ }
984
+ if (tokens?.refresh_token) {
985
+ res.cookie("refresh_token", tokens.refresh_token, {
986
+ httpOnly: true,
987
+ secure: false,
988
+ sameSite: "lax",
989
+ maxAge: 24 * 60 * 60 * 1e3,
990
+ // 24 hours
991
+ path: "/"
992
+ });
993
+ }
994
+ }
995
+ function toUserResponse(user) {
996
+ if (!user) return null;
997
+ return {
998
+ sub: user.id || user.keycloakId,
999
+ email: user.email,
1000
+ firstName: user.firstName,
1001
+ lastName: user.lastName,
1002
+ projectId: user.projectId,
1003
+ metadata: user.metadata,
1004
+ roles: user.roles
1005
+ };
1006
+ }
1007
+ function respondWithKeycloakError(res, err, fallback, status = 400) {
1008
+ const description = err?.response?.data?.error_description || err?.response?.data?.errorMessage || err?.message || fallback;
1009
+ return res.status(status).json({ ok: false, error: description });
1010
+ }
1011
+ function buildVerificationTemplate(token, options) {
1012
+ return `<a href="${getFrontendBaseUrl(options)}/auth/verify-email?token=${token}">Verify</a>`;
1013
+ }
1014
+ function buildResetTemplate(token, options) {
1015
+ return `<a href="${getFrontendBaseUrl(options)}/auth/reset-password?token=${token}">Reset</a>`;
1016
+ }
1017
+ function getFrontendBaseUrl(options) {
1018
+ if (options.frontendBaseUrl)
1019
+ return options.frontendBaseUrl.replace(/\/$/, "");
1020
+ const domain = process.env.ORG_DOMAIN?.replace(/\/$/, "");
1021
+ if (!domain) return "";
1022
+ return domain.startsWith("http") ? domain : `https://${domain}`;
1023
+ }
1024
+ async function sendRateLimitedEmail({
1025
+ emailService,
1026
+ user,
1027
+ subject,
1028
+ html
1029
+ }) {
1030
+ const can = emailService.canSend(user?.lastEmailSent || []);
1031
+ if (!can.ok) {
1032
+ return { rateLimited: true, waitMs: can.waitMs };
1033
+ }
1034
+ await emailService.send(user.email, subject, html);
1035
+ user.lastEmailSent = [...user.lastEmailSent || [], /* @__PURE__ */ new Date()];
1036
+ await user.save();
1037
+ return { rateLimited: false };
1038
+ }
1039
+ function generateTokens(user) {
1040
+ const accessToken = import_jsonwebtoken4.default.sign(
1041
+ {
1042
+ sub: user.id.toString(),
1043
+ email: user.email,
1044
+ roles: user.roles || [],
1045
+ type: "user"
1046
+ },
1047
+ process.env.JWT_SECRET,
1048
+ { expiresIn: "1h" }
1049
+ );
1050
+ const refreshToken = import_jsonwebtoken4.default.sign(
1051
+ { sub: user._id.toString() },
1052
+ process.env.JWT_SECRET,
1053
+ { expiresIn: "30d" }
1054
+ );
1055
+ return { access_token: accessToken, refresh_token: refreshToken };
1056
+ }
1057
+
1058
+ // src/express/dashboards.routes.ts
1059
+ var import_express2 = __toESM(require("express"), 1);
1060
+ function createDashboardRouter(options) {
1061
+ const r = (0, import_express2.Router)();
1062
+ const kc = new AuthAdminService();
1063
+ r.use(import_express2.default.json());
1064
+ r.post("/", requireAuth(), async (req, res, next) => {
1065
+ try {
1066
+ const { slug, isPublic, authFlow, orgDomain } = req.body || {};
1067
+ const redirectUris = [`https://${slug}.${orgDomain}/*`];
1068
+ const created = await kc.createClient(slug, redirectUris, !!isPublic);
1069
+ if (authFlow || isPublic != null) {
1070
+ await kc.updateClient(created.id, {
1071
+ authenticationFlowBindingOverrides: authFlow ? { browser: authFlow } : void 0,
1072
+ registrationAllowed: !!isPublic
1073
+ });
1074
+ }
1075
+ res.json({ clientId: created.clientId });
1076
+ } catch (e) {
1077
+ next(e);
1078
+ }
1079
+ });
1080
+ return r;
1081
+ }
1082
+
1083
+ // src/express/email.routes.ts
1084
+ var import_express3 = require("express");
1085
+ function createEmailRouter(options) {
1086
+ const r = (0, import_express3.Router)();
1087
+ r.get(
1088
+ "/verify",
1089
+ (req, res) => res.json({ ok: true, token: req.query.token })
1090
+ );
1091
+ return r;
1092
+ }
1093
+
1094
+ // src/express/projects.routes.ts
1095
+ var import_express4 = require("express");
1096
+
1097
+ // src/services/projects.service.ts
1098
+ var import_crypto2 = require("crypto");
1099
+
1100
+ // src/models/moduleConnection.model.ts
1101
+ var import_mongoose5 = __toESM(require("mongoose"), 1);
1102
+ var ModuleItemSchema = new import_mongoose5.default.Schema(
1103
+ { id: { type: String, required: true } },
1104
+ { _id: false }
1105
+ );
1106
+ var ModuleConnectionSchema = new import_mongoose5.default.Schema(
1107
+ {
1108
+ projectId: { type: String, required: true, index: true },
1109
+ modules: {
1110
+ data: { type: [ModuleItemSchema], default: [] },
1111
+ integration: { type: [ModuleItemSchema], default: [] },
1112
+ storage: { type: [ModuleItemSchema], default: [] }
1113
+ }
1114
+ },
1115
+ { timestamps: true, collection: "module_connection" }
1116
+ );
1117
+ var ModuleConnection = import_mongoose5.default.model(
1118
+ "ModuleConnection",
1119
+ ModuleConnectionSchema
1120
+ );
1121
+
1122
+ // src/models/project.model.ts
1123
+ var import_mongoose6 = __toESM(require("mongoose"), 1);
1124
+ var ProjectSchema = new import_mongoose6.default.Schema(
1125
+ {
1126
+ _id: { type: String, required: true },
1127
+ org_id: { type: String, required: true, index: true },
1128
+ name: { type: String, required: true },
1129
+ description: { type: String },
1130
+ secret: { type: String, required: true }
1131
+ },
1132
+ { timestamps: true, collection: "projects" }
1133
+ );
1134
+ var Project = import_mongoose6.default.model("Project", ProjectSchema);
1135
+
1136
+ // src/services/projects.service.ts
1137
+ var ProjectsService = class {
1138
+ async create(org_id, name, description) {
1139
+ const _id = (0, import_crypto2.randomUUID)();
1140
+ const secret = (0, import_crypto2.randomUUID)();
1141
+ const p = await Project.create({ _id, org_id, name, description, secret });
1142
+ await ModuleConnection.create({
1143
+ projectId: _id,
1144
+ modules: { data: [], integration: [], storage: [] }
1145
+ });
1146
+ return p.toObject();
1147
+ }
1148
+ async list(org_id) {
1149
+ return Project.find({ org_id }).lean();
1150
+ }
1151
+ async get(org_id, id) {
1152
+ return Project.findOne({ org_id, _id: id }).lean();
1153
+ }
1154
+ async update(org_id, id, patch) {
1155
+ return Project.findOneAndUpdate(
1156
+ { org_id, _id: id },
1157
+ { $set: patch },
1158
+ { new: true }
1159
+ ).lean();
1160
+ }
1161
+ async remove(org_id, id) {
1162
+ await Project.deleteOne({ org_id, _id: id });
1163
+ await ModuleConnection.deleteMany({ projectId: id });
1164
+ return { ok: true };
1165
+ }
1166
+ };
1167
+
1168
+ // src/express/projects.routes.ts
1169
+ function createProjectsRouter(options) {
1170
+ const r = (0, import_express4.Router)();
1171
+ const svc = new ProjectsService();
1172
+ r.post("/create", requireAuth(), async (req, res) => {
1173
+ const { org_id, name, description } = req.body || {};
1174
+ const p = await svc.create(org_id, name, description);
1175
+ res.json(p);
1176
+ });
1177
+ r.get("/:org_id", requireAuth(), async (req, res) => {
1178
+ res.json(await svc.list(req.params.org_id));
1179
+ });
1180
+ r.get("/:org_id/:id", requireAuth(), async (req, res) => {
1181
+ res.json(await svc.get(req.params.org_id, req.params.id));
1182
+ });
1183
+ r.put("/:org_id/:id", requireAuth(), async (req, res) => {
1184
+ res.json(
1185
+ await svc.update(req.params.org_id, req.params.id, req.body || {})
1186
+ );
1187
+ });
1188
+ r.delete("/:org_id/:id", requireAuth(), async (req, res) => {
1189
+ res.json(await svc.remove(req.params.org_id, req.params.id));
1190
+ });
1191
+ return r;
1192
+ }
1193
+
1194
+ // src/express/admin/admin.routes.ts
1195
+ var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
1196
+ var import_crypto3 = require("crypto");
1197
+ var import_express5 = __toESM(require("express"), 1);
1198
+
1199
+ // src/core/utils.ts
1200
+ function hasAnyRole(session, roles) {
1201
+ if (!session || !session.roles || !Array.isArray(roles) || roles.length === 0) {
1202
+ return false;
1203
+ }
1204
+ return roles.some((role) => session.roles.includes(role));
1205
+ }
1206
+
1207
+ // src/middlewares/requireRole.ts
1208
+ function requireRole(...roles) {
1209
+ return (req, res, next) => {
1210
+ const user = req.user;
1211
+ if (!user) {
1212
+ return res.status(401).json({ error: "Unauthorized" });
1213
+ }
1214
+ if (!roles || roles.length === 0) {
1215
+ return next();
1216
+ }
1217
+ if (!hasAnyRole(user, roles)) {
1218
+ return res.status(403).json({
1219
+ error: `Requires one of roles: ${roles.join(", ")}`,
1220
+ required: roles,
1221
+ userRoles: user.roles
1222
+ });
1223
+ }
1224
+ next();
1225
+ };
1226
+ }
1227
+
1228
+ // src/models/permissions.model.ts
1229
+ var import_mongoose7 = __toESM(require("mongoose"), 1);
1230
+ var PermissionsSchema = new import_mongoose7.Schema(
1231
+ {
1232
+ id: { type: String, required: true, index: true },
1233
+ orgId: { type: String, default: null, index: true },
1234
+ key: { type: String, required: true },
1235
+ type: { type: String, required: true },
1236
+ apiId: { type: String, required: false },
1237
+ description: { type: String },
1238
+ isInternal: { type: Boolean, default: false }
1239
+ },
1240
+ {
1241
+ timestamps: true
1242
+ }
1243
+ );
1244
+ PermissionsSchema.index({ orgId: 1, key: 1 }, { unique: true });
1245
+ var PermissionsModel = import_mongoose7.default.model(
1246
+ "Permissions",
1247
+ PermissionsSchema,
1248
+ "permissions"
1249
+ );
1250
+
1251
+ // src/express/admin/admin.routes.ts
1252
+ function resolveOrgId(req) {
1253
+ const user = req.user || {};
1254
+ const fromUser = user.orgId || user.org_id || null;
1255
+ const fromQuery = req.query.orgId || null;
1256
+ const fromBody = req.body && req.body.orgId || null;
1257
+ return fromQuery || fromBody || fromUser;
1258
+ }
1259
+ function resolveProjectId(req) {
1260
+ const user = req.user || {};
1261
+ const fromUser = user.projectId || null;
1262
+ const fromQuery = req.query.projectId || null;
1263
+ const fromBody = req.body && req.body.projectId || null;
1264
+ return fromQuery || fromBody || fromUser;
1265
+ }
1266
+ function createAdminRouter(_options = {}) {
1267
+ const r = (0, import_express5.Router)();
1268
+ r.use(import_express5.default.json());
1269
+ r.use(import_express5.default.urlencoded({ extended: true }));
1270
+ const adminGuards = [requireAuth(), requireRole("platform_admin")];
1271
+ r.post(
1272
+ "/users",
1273
+ ...adminGuards,
1274
+ async (req, res) => {
1275
+ const {
1276
+ firstName,
1277
+ lastName,
1278
+ email: emailAddress,
1279
+ password,
1280
+ emailVerified = false,
1281
+ roles = []
1282
+ } = req.body || {};
1283
+ const projectId = resolveProjectId(req);
1284
+ try {
1285
+ const hashedPassword = password ? await import_bcryptjs2.default.hash(password, 10) : void 0;
1286
+ const user = await OrgUser.create({
1287
+ id: (0, import_crypto3.randomUUID)(),
1288
+ email: emailAddress,
1289
+ orgId: process.env.ORG_ID,
1290
+ firstName,
1291
+ lastName,
1292
+ projectId,
1293
+ emailVerified,
1294
+ metadata: [],
1295
+ passwordHash: hashedPassword,
1296
+ roles
1297
+ });
1298
+ return res.json({
1299
+ id: user.id,
1300
+ email: user.email,
1301
+ message: "Verification email sent. Please check your inbox."
1302
+ });
1303
+ } catch (err) {
1304
+ console.error("Create user error:", err);
1305
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1306
+ }
1307
+ }
1308
+ );
1309
+ r.delete(
1310
+ "/users",
1311
+ ...adminGuards,
1312
+ async (req, res) => {
1313
+ try {
1314
+ const userId = req?.body?.id || req?.query?.id;
1315
+ if (!userId) {
1316
+ return res.status(400).json({
1317
+ error: "VALIDATION_ERROR",
1318
+ message: "UserId is required (send in body.id or ?id=...)"
1319
+ });
1320
+ }
1321
+ const deleted = await OrgUser.findOneAndDelete({ id: userId }).exec();
1322
+ if (!deleted) {
1323
+ return res.status(404).json({
1324
+ error: "NOT_FOUND",
1325
+ message: "User not found or already deleted"
1326
+ });
1327
+ }
1328
+ return res.status(200).json({
1329
+ ok: true,
1330
+ message: "User deleted successfully",
1331
+ deletedUser: {
1332
+ id: deleted.id,
1333
+ firstName: deleted.firstName,
1334
+ email: deleted.email,
1335
+ orgId: deleted.orgId
1336
+ }
1337
+ });
1338
+ } catch (err) {
1339
+ console.error("Delete user error:", err);
1340
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1341
+ }
1342
+ }
1343
+ );
1344
+ r.put(
1345
+ "/users/:id",
1346
+ ...adminGuards,
1347
+ async (req, res) => {
1348
+ const userId = req.params.id;
1349
+ const {
1350
+ firstName,
1351
+ lastName,
1352
+ email: emailAddress,
1353
+ password,
1354
+ emailVerified,
1355
+ roles
1356
+ } = req.body || {};
1357
+ try {
1358
+ const existingUser = await OrgUser.findOne({
1359
+ id: userId,
1360
+ orgId: process.env.ORG_ID
1361
+ });
1362
+ if (!existingUser) {
1363
+ return res.status(404).json({ error: "USER_NOT_FOUND" });
1364
+ }
1365
+ if (firstName !== void 0) existingUser.firstName = firstName;
1366
+ if (lastName !== void 0) existingUser.lastName = lastName;
1367
+ if (emailAddress !== void 0) existingUser.email = emailAddress;
1368
+ if (emailVerified !== void 0)
1369
+ existingUser.emailVerified = emailVerified;
1370
+ if (roles !== void 0) existingUser.roles = roles;
1371
+ if (password) {
1372
+ existingUser.passwordHash = await import_bcryptjs2.default.hash(password, 10);
1373
+ }
1374
+ await existingUser.save();
1375
+ return res.json({
1376
+ id: existingUser.id,
1377
+ email: existingUser.email,
1378
+ message: "User updated successfully."
1379
+ });
1380
+ } catch (err) {
1381
+ console.error("Update user error:", err);
1382
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1383
+ }
1384
+ }
1385
+ );
1386
+ r.get(
1387
+ "/permissions",
1388
+ ...adminGuards,
1389
+ async (req, res) => {
1390
+ try {
1391
+ const orgId = resolveOrgId(req);
1392
+ const filter = {};
1393
+ if (orgId !== null) {
1394
+ filter.orgId = orgId;
1395
+ } else {
1396
+ filter.orgId = null;
1397
+ }
1398
+ const items = await PermissionsModel.find(filter).lean().exec();
1399
+ return res.json(items);
1400
+ } catch (err) {
1401
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1402
+ }
1403
+ }
1404
+ );
1405
+ r.post(
1406
+ "/permissions",
1407
+ ...adminGuards,
1408
+ async (req, res) => {
1409
+ try {
1410
+ const orgId = resolveOrgId(req);
1411
+ const {
1412
+ key,
1413
+ type,
1414
+ apiId,
1415
+ description,
1416
+ isInternal = false
1417
+ } = req.body || {};
1418
+ if (!key || !type) {
1419
+ return res.status(400).json({
1420
+ error: "VALIDATION_ERROR",
1421
+ message: "permission key, and permission type are required"
1422
+ });
1423
+ }
1424
+ const id = (0, import_crypto3.randomUUID)();
1425
+ const permission = await PermissionsModel.create({
1426
+ id,
1427
+ orgId: orgId ?? null,
1428
+ key,
1429
+ type,
1430
+ apiId,
1431
+ description,
1432
+ isInternal: !!isInternal
1433
+ });
1434
+ await RolePermissionModel.findOneAndUpdate(
1435
+ { orgId: orgId ?? null, role: "platform_admin" },
1436
+ { $addToSet: { permissions: key } },
1437
+ { upsert: true, new: true }
1438
+ ).exec();
1439
+ return res.status(201).json(permission);
1440
+ } catch (err) {
1441
+ if (err && err.code === 11e3) {
1442
+ return res.status(409).json({
1443
+ error: "DUPLICATE_PERMISSION",
1444
+ message: "Permission key already exists for this org"
1445
+ });
1446
+ }
1447
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1448
+ }
1449
+ }
1450
+ );
1451
+ r.put(
1452
+ "/permissions/:id",
1453
+ ...adminGuards,
1454
+ async (req, res) => {
1455
+ try {
1456
+ const orgId = resolveOrgId(req);
1457
+ const permissionId = req.params.id;
1458
+ const { key, type, apiId, description, isInternal } = req.body || {};
1459
+ const existing = await PermissionsModel.findOne({
1460
+ id: permissionId,
1461
+ orgId: orgId ?? null
1462
+ });
1463
+ if (!existing) {
1464
+ return res.status(404).json({
1465
+ error: "NOT_FOUND",
1466
+ message: "Permission does not exist"
1467
+ });
1468
+ }
1469
+ const oldKey = existing.key;
1470
+ if (key !== void 0) existing.key = key;
1471
+ if (type !== void 0) existing.type = type;
1472
+ if (apiId !== void 0) existing.apiId = apiId;
1473
+ if (description !== void 0) existing.description = description;
1474
+ if (isInternal !== void 0) existing.isInternal = !!isInternal;
1475
+ await existing.save();
1476
+ if (oldKey !== key) {
1477
+ await RolePermissionModel.updateMany(
1478
+ {
1479
+ orgId: orgId ?? null,
1480
+ permissions: oldKey
1481
+ },
1482
+ {
1483
+ $pull: { permissions: oldKey }
1484
+ }
1485
+ );
1486
+ await RolePermissionModel.updateMany(
1487
+ {
1488
+ orgId: orgId ?? null
1489
+ },
1490
+ {
1491
+ $addToSet: { permissions: key }
1492
+ }
1493
+ );
1494
+ }
1495
+ return res.json(existing);
1496
+ } catch (err) {
1497
+ if (err && err.code === 11e3) {
1498
+ return res.status(409).json({
1499
+ error: "DUPLICATE_PERMISSION",
1500
+ message: "Permission key already exists for this org"
1501
+ });
1502
+ }
1503
+ console.error("Update permission error:", err);
1504
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1505
+ }
1506
+ }
1507
+ );
1508
+ r.delete(
1509
+ "/permissions",
1510
+ ...adminGuards,
1511
+ async (req, res) => {
1512
+ try {
1513
+ const permissionId = req?.body?.id || req?.query?.id;
1514
+ if (!permissionId) {
1515
+ return res.status(400).json({
1516
+ error: "VALIDATION_ERROR",
1517
+ message: "Permission id is required (send in body.id or ?id=...)"
1518
+ });
1519
+ }
1520
+ const existing = await PermissionsModel.findOne({ id: permissionId });
1521
+ if (!existing) {
1522
+ return res.status(404).json({
1523
+ error: "NOT_FOUND",
1524
+ message: "Permission not found or already deleted"
1525
+ });
1526
+ }
1527
+ const { key, orgId } = existing;
1528
+ await PermissionsModel.deleteOne({ id: permissionId });
1529
+ await RolePermissionModel.updateMany(
1530
+ { orgId: orgId ?? null },
1531
+ { $pull: { permissions: key } }
1532
+ );
1533
+ return res.status(200).json({
1534
+ ok: true,
1535
+ message: "Permission deleted successfully",
1536
+ deletedPermission: {
1537
+ id: existing.id,
1538
+ key: existing.key,
1539
+ type: existing.type,
1540
+ apiId: existing.apiId,
1541
+ description: existing.description,
1542
+ isInternal: existing.isInternal,
1543
+ orgId: existing.orgId
1544
+ }
1545
+ });
1546
+ } catch (err) {
1547
+ console.error("Delete permission error:", err);
1548
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1549
+ }
1550
+ }
1551
+ );
1552
+ r.get(
1553
+ "/roles",
1554
+ ...adminGuards,
1555
+ async (req, res) => {
1556
+ try {
1557
+ const orgId = resolveOrgId(req);
1558
+ const filter = {};
1559
+ if (orgId !== null) {
1560
+ filter.orgId = orgId;
1561
+ } else {
1562
+ filter.orgId = null;
1563
+ }
1564
+ const roles = await RolePermissionModel.find(filter).lean().exec();
1565
+ return res.json(roles);
1566
+ } catch (err) {
1567
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1568
+ }
1569
+ }
1570
+ );
1571
+ r.post(
1572
+ "/roles",
1573
+ ...adminGuards,
1574
+ async (req, res) => {
1575
+ try {
1576
+ const orgId = resolveOrgId(req);
1577
+ const { role, permissions } = req.body || {};
1578
+ if (!role || !Array.isArray(permissions)) {
1579
+ return res.status(400).json({
1580
+ error: "VALIDATION_ERROR",
1581
+ message: "role and permissions[] are required"
1582
+ });
1583
+ }
1584
+ const id = (0, import_crypto3.randomUUID)();
1585
+ const doc = await RolePermissionModel.findOneAndUpdate(
1586
+ { orgId: orgId ?? null, role },
1587
+ { $set: { permissions } },
1588
+ { upsert: true, new: true }
1589
+ ).exec();
1590
+ return res.status(200).json(doc);
1591
+ } catch (err) {
1592
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1593
+ }
1594
+ }
1595
+ );
1596
+ r.put(
1597
+ "/roles/:id",
1598
+ ...adminGuards,
1599
+ async (req, res) => {
1600
+ try {
1601
+ const orgId = resolveOrgId(req);
1602
+ const roleId = req.params.id;
1603
+ const { role: newRoleName, permissions } = req.body || {};
1604
+ if (!newRoleName || !Array.isArray(permissions)) {
1605
+ return res.status(400).json({
1606
+ error: "VALIDATION_ERROR",
1607
+ message: "role and permissions are required"
1608
+ });
1609
+ }
1610
+ const existing = await RolePermissionModel.findById(roleId);
1611
+ if (!existing) {
1612
+ return res.status(404).json({
1613
+ error: "ROLE_NOT_FOUND",
1614
+ message: "Role does not exist"
1615
+ });
1616
+ }
1617
+ const oldRoleName = existing.role;
1618
+ existing.role = newRoleName;
1619
+ existing.permissions = permissions;
1620
+ await existing.save();
1621
+ if (oldRoleName !== newRoleName) {
1622
+ await OrgUser.updateMany(
1623
+ {
1624
+ orgId: orgId ?? null,
1625
+ roles: oldRoleName
1626
+ },
1627
+ {
1628
+ $pull: { roles: oldRoleName }
1629
+ }
1630
+ );
1631
+ await OrgUser.updateMany(
1632
+ {
1633
+ orgId: orgId ?? null,
1634
+ roles: { $ne: newRoleName }
1635
+ // avoid duplicates
1636
+ },
1637
+ {
1638
+ $addToSet: { roles: newRoleName }
1639
+ }
1640
+ );
1641
+ }
1642
+ return res.status(200).json(existing);
1643
+ } catch (err) {
1644
+ console.error("Update role error:", err);
1645
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1646
+ }
1647
+ }
1648
+ );
1649
+ r.delete(
1650
+ "/roles",
1651
+ ...adminGuards,
1652
+ async (req, res) => {
1653
+ try {
1654
+ const roleId = req?.body?.id || req?.query?.id;
1655
+ if (!roleId) {
1656
+ return res.status(400).json({
1657
+ error: "VALIDATION_ERROR",
1658
+ message: "Role _id is required (send in body.id or ?id=...)"
1659
+ });
1660
+ }
1661
+ if (!/^[0-9a-fA-F]{24}$/.test(roleId)) {
1662
+ return res.status(400).json({
1663
+ error: "VALIDATION_ERROR",
1664
+ message: "Invalid role _id format"
1665
+ });
1666
+ }
1667
+ const deleted = await RolePermissionModel.findByIdAndDelete(roleId).exec();
1668
+ if (!deleted) {
1669
+ return res.status(404).json({
1670
+ error: "NOT_FOUND",
1671
+ message: "Role not found or already deleted"
1672
+ });
1673
+ }
1674
+ return res.status(200).json({
1675
+ ok: true,
1676
+ message: "Role deleted successfully",
1677
+ deletedRole: {
1678
+ _id: deleted._id,
1679
+ role: deleted.role,
1680
+ orgId: deleted.orgId
1681
+ }
1682
+ });
1683
+ } catch (err) {
1684
+ console.error("Delete role error:", err);
1685
+ return res.status(500).json({ error: "INTERNAL_ERROR" });
1686
+ }
1687
+ }
1688
+ );
1689
+ return r;
1690
+ }
1691
+ // Annotate the CommonJS export names for ESM import in node:
1692
+ 0 && (module.exports = {
1693
+ createAdminRouter,
1694
+ createAuthRouter,
1695
+ createDashboardRouter,
1696
+ createEmailRouter,
1697
+ createProjectsRouter
1698
+ });
1699
+ //# sourceMappingURL=index.cjs.map