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