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.
- package/LICENSE +3 -0
- package/README.md +12 -0
- package/dist/express/index.cjs +1699 -0
- package/dist/express/index.cjs.map +1 -0
- package/dist/express/index.d.cts +3 -0
- package/dist/express/index.d.ts +3 -0
- package/dist/express/index.js +1658 -0
- package/dist/express/index.js.map +1 -0
- package/dist/index-KOTz0ZcH.d.cts +23 -0
- package/dist/index-KOTz0ZcH.d.ts +23 -0
- package/dist/index.cjs +2266 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1420 -0
- package/dist/index.d.ts +1420 -0
- package/dist/index.js +2204 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.cjs +1720 -0
- package/dist/nest/index.cjs.map +1 -0
- package/dist/nest/index.d.cts +28 -0
- package/dist/nest/index.d.ts +28 -0
- package/dist/nest/index.js +1683 -0
- package/dist/nest/index.js.map +1 -0
- package/package.json +96 -0
|
@@ -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
|