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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2204 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
8
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
9
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
10
|
+
if (decorator = decorators[i])
|
|
11
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
12
|
+
if (kind && result) __defProp(target, key, result);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/express/index.ts
|
|
17
|
+
var express_exports = {};
|
|
18
|
+
__export(express_exports, {
|
|
19
|
+
createAdminRouter: () => createAdminRouter,
|
|
20
|
+
createAuthRouter: () => createAuthRouter,
|
|
21
|
+
createDashboardRouter: () => createDashboardRouter,
|
|
22
|
+
createEmailRouter: () => createEmailRouter,
|
|
23
|
+
createProjectsRouter: () => createProjectsRouter
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/express/auth.routes.ts
|
|
27
|
+
import bcrypt2 from "bcryptjs";
|
|
28
|
+
import { randomUUID } from "crypto";
|
|
29
|
+
import express, { Router } from "express";
|
|
30
|
+
import jwt4 from "jsonwebtoken";
|
|
31
|
+
|
|
32
|
+
// src/config/loadConfig.ts
|
|
33
|
+
function loadConfig() {
|
|
34
|
+
return {
|
|
35
|
+
orgDomain: process.env.ORG_DOMAIN,
|
|
36
|
+
orgId: process.env.ORG_ID,
|
|
37
|
+
email: {
|
|
38
|
+
host: process.env.EMAIL_HOST || "smtp.postmarkapp.com",
|
|
39
|
+
port: process.env.EMAIL_PORT ? Number(process.env.EMAIL_PORT) : 587,
|
|
40
|
+
secure: (process.env.EMAIL_SECURE || "false") === "true",
|
|
41
|
+
user: process.env.EMAIL_USER,
|
|
42
|
+
pass: process.env.EMAIL_PASSWORD,
|
|
43
|
+
from: process.env.EMAIL_FROM,
|
|
44
|
+
jwtSecret: process.env.EMAIL_JWT_SECRET
|
|
45
|
+
},
|
|
46
|
+
cookies: {
|
|
47
|
+
domain: process.env.COOKIE_DOMAIN,
|
|
48
|
+
secure: (process.env.COOKIE_SECURE || "true") === "true",
|
|
49
|
+
accessTtlMs: 24 * 60 * 60 * 1e3,
|
|
50
|
+
refreshTtlMs: 7 * 24 * 60 * 60 * 1e3
|
|
51
|
+
},
|
|
52
|
+
oidc: {
|
|
53
|
+
jwtSecret: process.env.JWT_SECRET
|
|
54
|
+
},
|
|
55
|
+
aws: {
|
|
56
|
+
bucket: process.env.AWS_S3_BUCKET,
|
|
57
|
+
region: process.env.AWS_REGION,
|
|
58
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
59
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/config/index.ts
|
|
65
|
+
var config = loadConfig();
|
|
66
|
+
function configureAuthX(overrides = {}) {
|
|
67
|
+
return deepMerge(config, overrides);
|
|
68
|
+
}
|
|
69
|
+
function deepMerge(target, source) {
|
|
70
|
+
if (!source) {
|
|
71
|
+
return target;
|
|
72
|
+
}
|
|
73
|
+
for (const key of Object.keys(source)) {
|
|
74
|
+
const value = source[key];
|
|
75
|
+
if (value === void 0) continue;
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
target[key] = [...value];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (isPlainObject(value)) {
|
|
81
|
+
if (!isPlainObject(target[key])) {
|
|
82
|
+
target[key] = {};
|
|
83
|
+
}
|
|
84
|
+
deepMerge(target[key], value);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
target[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/core/roles.config.ts
|
|
96
|
+
var PLATFORM_ROLES = [
|
|
97
|
+
{
|
|
98
|
+
role: "platform_admin",
|
|
99
|
+
permissions: [
|
|
100
|
+
"projects.create",
|
|
101
|
+
"projects.read",
|
|
102
|
+
"projects.update",
|
|
103
|
+
"projects.delete",
|
|
104
|
+
"users.manage",
|
|
105
|
+
"api.manage"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
role: "platform_manager",
|
|
110
|
+
permissions: [
|
|
111
|
+
"projects.read",
|
|
112
|
+
"projects.update",
|
|
113
|
+
"users.read"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
role: "platform_user",
|
|
118
|
+
permissions: ["projects.read"]
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
function getPermissionsForRoles(roles) {
|
|
122
|
+
if (!Array.isArray(roles) || roles.length === 0) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const permissionSet = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const roleName of roles) {
|
|
127
|
+
const roleConfig = PLATFORM_ROLES.find((r) => r.role === roleName);
|
|
128
|
+
if (roleConfig && Array.isArray(roleConfig.permissions)) {
|
|
129
|
+
for (const perm of roleConfig.permissions) {
|
|
130
|
+
permissionSet.add(perm);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Array.from(permissionSet);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/core/session.ts
|
|
138
|
+
function buildSession(payload) {
|
|
139
|
+
const userId = payload?.sub || payload?.userId || payload?.id || "";
|
|
140
|
+
const email = payload?.email || payload?.email_address || "";
|
|
141
|
+
const roles = payload?.realm_access?.roles || payload?.roles || payload?.["cognito:groups"] || (Array.isArray(payload?.role) ? payload.role : []) || [];
|
|
142
|
+
const normalizedRoles = Array.isArray(roles) ? roles.map(String).filter(Boolean) : [];
|
|
143
|
+
const permissions = getPermissionsForRoles(normalizedRoles);
|
|
144
|
+
const session = {
|
|
145
|
+
userId,
|
|
146
|
+
email,
|
|
147
|
+
roles: normalizedRoles,
|
|
148
|
+
permissions
|
|
149
|
+
};
|
|
150
|
+
if (payload?.projectId) session.projectId = payload.projectId;
|
|
151
|
+
if (payload?.orgId) session.orgId = payload.orgId;
|
|
152
|
+
if (payload?.org_id) session.org_id = payload.org_id;
|
|
153
|
+
if (payload?.authType) session.authType = payload.authType;
|
|
154
|
+
Object.keys(payload || {}).forEach((key) => {
|
|
155
|
+
if (![
|
|
156
|
+
"sub",
|
|
157
|
+
"userId",
|
|
158
|
+
"id",
|
|
159
|
+
"email",
|
|
160
|
+
"email_address",
|
|
161
|
+
"realm_access",
|
|
162
|
+
"roles",
|
|
163
|
+
"cognito:groups",
|
|
164
|
+
"role",
|
|
165
|
+
"projectId",
|
|
166
|
+
"orgId",
|
|
167
|
+
"org_id",
|
|
168
|
+
"authType"
|
|
169
|
+
].includes(key)) {
|
|
170
|
+
session[key] = payload[key];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return session;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/models/user.model.ts
|
|
177
|
+
import mongoose from "mongoose";
|
|
178
|
+
import { v4 as uuid } from "uuid";
|
|
179
|
+
var MetadataSchema = new mongoose.Schema(
|
|
180
|
+
{
|
|
181
|
+
key: { type: String, required: true },
|
|
182
|
+
value: { type: mongoose.Schema.Types.Mixed, required: true }
|
|
183
|
+
},
|
|
184
|
+
{ _id: false }
|
|
185
|
+
);
|
|
186
|
+
var OrgUserSchema = new mongoose.Schema(
|
|
187
|
+
{
|
|
188
|
+
id: { type: String, default: uuid(), index: true },
|
|
189
|
+
email: { type: String, required: true, unique: true },
|
|
190
|
+
firstName: { type: String, required: true },
|
|
191
|
+
lastName: { type: String, required: true },
|
|
192
|
+
orgId: { type: String },
|
|
193
|
+
projectId: { type: String, required: true },
|
|
194
|
+
roles: { type: [String], default: [] },
|
|
195
|
+
emailVerified: { type: Boolean, default: false },
|
|
196
|
+
lastEmailSent: { type: [Date], default: [] },
|
|
197
|
+
lastPasswordReset: { type: Date },
|
|
198
|
+
metadata: { type: [MetadataSchema], default: [] },
|
|
199
|
+
passwordHash: { type: String }
|
|
200
|
+
},
|
|
201
|
+
{ timestamps: true, collection: "users" }
|
|
202
|
+
);
|
|
203
|
+
var OrgUser = mongoose.model("OrgUser", OrgUserSchema);
|
|
204
|
+
|
|
205
|
+
// src/utils/extract.ts
|
|
206
|
+
import { parse as parseCookie } from "cookie";
|
|
207
|
+
function extractToken(req, opts) {
|
|
208
|
+
const headerNames = opts?.headerNames ?? ["authorization", "token"];
|
|
209
|
+
const cookieNames = opts?.cookieNames ?? ["access_token", "authorization"];
|
|
210
|
+
const queryNames = opts?.queryNames ?? ["access_token", "token"];
|
|
211
|
+
for (const h of headerNames) {
|
|
212
|
+
const raw = req.headers[h];
|
|
213
|
+
if (raw) {
|
|
214
|
+
const lower = raw.toLowerCase();
|
|
215
|
+
if (lower.startsWith("bearer ")) return raw.slice(7).trim();
|
|
216
|
+
if (!raw.includes(" ")) return raw.trim();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const ch = req.headers["cookie"];
|
|
220
|
+
if (typeof ch === "string") {
|
|
221
|
+
const parsed = parseCookie(ch);
|
|
222
|
+
for (const c of cookieNames) if (parsed[c]) return parsed[c];
|
|
223
|
+
}
|
|
224
|
+
for (const q of queryNames) {
|
|
225
|
+
const v = req.query?.[q];
|
|
226
|
+
if (typeof v === "string" && v) return v;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
function readProjectId(req) {
|
|
231
|
+
const ch = req.headers["cookie"];
|
|
232
|
+
if (typeof ch === "string") {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = parseCookie(ch);
|
|
235
|
+
return parsed["projectId"] || null;
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/utils/jwt.ts
|
|
243
|
+
import jwt from "jsonwebtoken";
|
|
244
|
+
function verifyJwt(token) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
jwt.verify(
|
|
247
|
+
token,
|
|
248
|
+
process.env.JWT_SECRET,
|
|
249
|
+
// This is your shared secret (string)
|
|
250
|
+
{
|
|
251
|
+
algorithms: ["HS256"],
|
|
252
|
+
// Only allow HS256
|
|
253
|
+
complete: false
|
|
254
|
+
// We only want payload
|
|
255
|
+
},
|
|
256
|
+
(err, decoded) => {
|
|
257
|
+
if (err) {
|
|
258
|
+
reject(err);
|
|
259
|
+
} else {
|
|
260
|
+
resolve(decoded);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/middlewares/auth.middleware.ts
|
|
268
|
+
function requireAuth() {
|
|
269
|
+
return async (req, res, next) => {
|
|
270
|
+
try {
|
|
271
|
+
const apiKey = req.headers["x-api-key"] || req.headers["x-apikey"];
|
|
272
|
+
const userId = req.headers["x-user-id"] || req.headers["x-userId"];
|
|
273
|
+
if (apiKey) {
|
|
274
|
+
if (apiKey !== process.env.SERVER_API_KEY) {
|
|
275
|
+
return res.status(401).json({ error: "Invalid API key" });
|
|
276
|
+
}
|
|
277
|
+
if (!userId) {
|
|
278
|
+
return res.status(401).json({ error: "User Id is Required" });
|
|
279
|
+
}
|
|
280
|
+
const user = await OrgUser.findOne({ id: userId }).lean();
|
|
281
|
+
if (!user) {
|
|
282
|
+
return res.status(401).json({ error: "User not found" });
|
|
283
|
+
}
|
|
284
|
+
const session2 = buildSession({
|
|
285
|
+
sub: user.id.toString(),
|
|
286
|
+
email: user.email,
|
|
287
|
+
roles: user.roles || []
|
|
288
|
+
});
|
|
289
|
+
session2.authType = "api-key";
|
|
290
|
+
session2.projectId = readProjectId(req) || user.projectId || void 0;
|
|
291
|
+
req.user = session2;
|
|
292
|
+
return next();
|
|
293
|
+
}
|
|
294
|
+
const token = extractToken(req);
|
|
295
|
+
if (!token) {
|
|
296
|
+
return res.status(401).json({ error: "Missing token" });
|
|
297
|
+
}
|
|
298
|
+
const claims = await verifyJwt(token);
|
|
299
|
+
const session = buildSession(claims);
|
|
300
|
+
const pid = readProjectId(req);
|
|
301
|
+
if (pid) session.projectId = pid;
|
|
302
|
+
req.user = session;
|
|
303
|
+
next();
|
|
304
|
+
} catch (e) {
|
|
305
|
+
res.status(401).json({ error: e?.message || "Unauthorized" });
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function authorize(roles = []) {
|
|
310
|
+
return (req, res, next) => {
|
|
311
|
+
if (!roles || roles.length === 0) return next();
|
|
312
|
+
const user = req.user;
|
|
313
|
+
if (!user) {
|
|
314
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
315
|
+
}
|
|
316
|
+
console.log(user, "user");
|
|
317
|
+
const have = new Set((user.roles || []).map(String));
|
|
318
|
+
const ok = roles.some((r) => have.has(r));
|
|
319
|
+
if (!ok) {
|
|
320
|
+
return res.status(403).json({ error: `Requires one of roles: ${roles.join(", ")}` });
|
|
321
|
+
}
|
|
322
|
+
next();
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/middlewares/validators.ts
|
|
327
|
+
function isEmail(v) {
|
|
328
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
329
|
+
}
|
|
330
|
+
function isPasswordStrong(v) {
|
|
331
|
+
return typeof v === "string" && v.length >= 6 && /[A-Z]/.test(v) && /[^a-zA-Z0-9]/.test(v);
|
|
332
|
+
}
|
|
333
|
+
function validateSignup(req, res, next) {
|
|
334
|
+
const { firstName, lastName, email, password, projectId, metadata } = req.body || {};
|
|
335
|
+
if (!firstName || !lastName)
|
|
336
|
+
return res.status(400).json({ error: "firstName,lastName required" });
|
|
337
|
+
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
338
|
+
if (!isPasswordStrong(password))
|
|
339
|
+
return res.status(400).json({ error: "weak password" });
|
|
340
|
+
if (!projectId) return res.status(400).json({ error: "projectId required" });
|
|
341
|
+
if (!Array.isArray(metadata))
|
|
342
|
+
return res.status(400).json({ error: "metadata must be array" });
|
|
343
|
+
next();
|
|
344
|
+
}
|
|
345
|
+
function validateLogin(req, res, next) {
|
|
346
|
+
const { email, password } = req.body || {};
|
|
347
|
+
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
348
|
+
if (typeof password !== "string")
|
|
349
|
+
return res.status(400).json({ error: "password required" });
|
|
350
|
+
next();
|
|
351
|
+
}
|
|
352
|
+
function validateResetPassword(req, res, next) {
|
|
353
|
+
const { token, newPassword } = req.body || {};
|
|
354
|
+
if (!token) return res.status(400).json({ error: "token required" });
|
|
355
|
+
if (!isPasswordStrong(newPassword))
|
|
356
|
+
return res.status(400).json({ error: "weak password" });
|
|
357
|
+
next();
|
|
358
|
+
}
|
|
359
|
+
function validateResendEmail(req, res, next) {
|
|
360
|
+
const { email } = req.body || {};
|
|
361
|
+
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
362
|
+
next();
|
|
363
|
+
}
|
|
364
|
+
function validateSendInvite(req, res, next) {
|
|
365
|
+
const { email, role } = req.body || {};
|
|
366
|
+
if (!isEmail(email)) return res.status(400).json({ error: "invalid email" });
|
|
367
|
+
if (!["platform_user", "org_admin"].includes(role))
|
|
368
|
+
return res.status(400).json({ error: "invalid role" });
|
|
369
|
+
next();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/models/invite.model.ts
|
|
373
|
+
import mongoose2 from "mongoose";
|
|
374
|
+
var InviteSchema = new mongoose2.Schema(
|
|
375
|
+
{
|
|
376
|
+
id: { type: String, required: true, index: true },
|
|
377
|
+
email: { type: String, required: true },
|
|
378
|
+
role: {
|
|
379
|
+
type: String,
|
|
380
|
+
enum: ["platform_user", "org_admin"],
|
|
381
|
+
required: true
|
|
382
|
+
},
|
|
383
|
+
invitedBy: { type: String },
|
|
384
|
+
usedBy: { type: String },
|
|
385
|
+
isUsed: { type: Boolean, default: false },
|
|
386
|
+
usedAt: { type: Date },
|
|
387
|
+
expiresAt: { type: Date },
|
|
388
|
+
isExpired: { type: Boolean, default: false }
|
|
389
|
+
},
|
|
390
|
+
{ timestamps: true, collection: "invites" }
|
|
391
|
+
);
|
|
392
|
+
var Invite = mongoose2.model("Invite", InviteSchema);
|
|
393
|
+
|
|
394
|
+
// src/services/auth-admin.service.ts
|
|
395
|
+
import bcrypt from "bcrypt";
|
|
396
|
+
import jwt2 from "jsonwebtoken";
|
|
397
|
+
|
|
398
|
+
// src/models/client.model.ts
|
|
399
|
+
import mongoose3, { Schema } from "mongoose";
|
|
400
|
+
var ClientSchema = new Schema(
|
|
401
|
+
{
|
|
402
|
+
clientId: {
|
|
403
|
+
type: String,
|
|
404
|
+
required: true,
|
|
405
|
+
unique: true,
|
|
406
|
+
index: true
|
|
407
|
+
},
|
|
408
|
+
redirectUris: {
|
|
409
|
+
type: [String],
|
|
410
|
+
default: []
|
|
411
|
+
},
|
|
412
|
+
publicClient: {
|
|
413
|
+
type: Boolean,
|
|
414
|
+
default: false
|
|
415
|
+
},
|
|
416
|
+
// Optional: if you want confidential clients
|
|
417
|
+
secret: {
|
|
418
|
+
type: String,
|
|
419
|
+
required: function() {
|
|
420
|
+
return !this.publicClient;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
timestamps: true
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
var ClientModel = mongoose3.models.Client || mongoose3.model("Client", ClientSchema);
|
|
429
|
+
|
|
430
|
+
// src/models/rolePermission.model.ts
|
|
431
|
+
import mongoose4, { Schema as Schema2 } from "mongoose";
|
|
432
|
+
var RolePermissionSchema = new Schema2(
|
|
433
|
+
{
|
|
434
|
+
orgId: { type: String, default: null, index: true },
|
|
435
|
+
role: { type: String, required: true },
|
|
436
|
+
permissions: { type: [String], default: [] }
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
timestamps: true
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
RolePermissionSchema.index({ orgId: 1, role: 1 }, { unique: true });
|
|
443
|
+
var RolePermissionModel = mongoose4.model(
|
|
444
|
+
"RolePermission",
|
|
445
|
+
RolePermissionSchema,
|
|
446
|
+
"role_permissions"
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// src/services/auth-admin.service.ts
|
|
450
|
+
var AuthAdminService = class {
|
|
451
|
+
token;
|
|
452
|
+
async getAdminToken() {
|
|
453
|
+
return this.ensureAdminToken();
|
|
454
|
+
}
|
|
455
|
+
// -------------------------------------------------------------------
|
|
456
|
+
// CLIENTS
|
|
457
|
+
// -------------------------------------------------------------------
|
|
458
|
+
async createClient(clientId, redirectUris = [], publicClient = false) {
|
|
459
|
+
const client = await ClientModel.create({
|
|
460
|
+
clientId,
|
|
461
|
+
redirectUris,
|
|
462
|
+
publicClient
|
|
463
|
+
});
|
|
464
|
+
return client;
|
|
465
|
+
}
|
|
466
|
+
async updateClient(id, patch) {
|
|
467
|
+
await ClientModel.findByIdAndUpdate(id, patch);
|
|
468
|
+
}
|
|
469
|
+
// -------------------------------------------------------------------
|
|
470
|
+
// USERS
|
|
471
|
+
// -------------------------------------------------------------------
|
|
472
|
+
async listUsersInRealm(_realm, filter) {
|
|
473
|
+
return OrgUser.find(filter || {});
|
|
474
|
+
}
|
|
475
|
+
async getUserById(userId) {
|
|
476
|
+
return OrgUser.findOne({ id: userId });
|
|
477
|
+
}
|
|
478
|
+
async isUserEmailVerified(userId) {
|
|
479
|
+
const user = await OrgUser.findOne({ id: userId });
|
|
480
|
+
return user?.emailVerified;
|
|
481
|
+
}
|
|
482
|
+
async createUserInRealm(payload) {
|
|
483
|
+
const hashedPassword = payload.credentials?.[0]?.value ? await bcrypt.hash(payload.credentials[0].value, 10) : void 0;
|
|
484
|
+
const user = await OrgUser.create({
|
|
485
|
+
username: payload.username,
|
|
486
|
+
email: payload.email,
|
|
487
|
+
firstName: payload.firstName,
|
|
488
|
+
lastName: payload.lastName,
|
|
489
|
+
projectId: payload.projectId,
|
|
490
|
+
emailVerified: payload.emailVerified || false,
|
|
491
|
+
passwordHash: hashedPassword,
|
|
492
|
+
enabled: true
|
|
493
|
+
});
|
|
494
|
+
return user;
|
|
495
|
+
}
|
|
496
|
+
async assignRealmRole(userId, roleName) {
|
|
497
|
+
const role = await RolePermissionModel.findOne({ name: roleName });
|
|
498
|
+
if (!role) throw new Error(`Role not found: ${roleName}`);
|
|
499
|
+
await OrgUser.findOneAndUpdate(
|
|
500
|
+
{ id: userId },
|
|
501
|
+
{
|
|
502
|
+
$addToSet: { roles: role._id }
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
async updateUserEmailVerified(userId, emailVerified) {
|
|
507
|
+
await OrgUser.findOneAndUpdate({ id: userId }, { emailVerified });
|
|
508
|
+
}
|
|
509
|
+
async updateUserPassword(userId, newPassword) {
|
|
510
|
+
const hashed = await bcrypt.hash(newPassword, 10);
|
|
511
|
+
await OrgUser.findOneAndUpdate({ id: userId }, { password: hashed });
|
|
512
|
+
}
|
|
513
|
+
// -------------------------------------------------------------------
|
|
514
|
+
// ADMIN TOKEN (self-issued JWT)
|
|
515
|
+
// -------------------------------------------------------------------
|
|
516
|
+
async ensureAdminToken() {
|
|
517
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
518
|
+
if (this.token && this.token.exp - 30 > now) {
|
|
519
|
+
return this.token.accessToken;
|
|
520
|
+
}
|
|
521
|
+
const payload = {
|
|
522
|
+
type: "admin",
|
|
523
|
+
system: true
|
|
524
|
+
};
|
|
525
|
+
const accessToken = jwt2.sign(payload, process.env.JWT_SECRET, {
|
|
526
|
+
expiresIn: "1h"
|
|
527
|
+
});
|
|
528
|
+
this.token = {
|
|
529
|
+
accessToken,
|
|
530
|
+
exp: now + 3600
|
|
531
|
+
};
|
|
532
|
+
return this.token.accessToken;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/services/email.service.ts
|
|
537
|
+
import jwt3 from "jsonwebtoken";
|
|
538
|
+
import nodemailer from "nodemailer";
|
|
539
|
+
var EmailService = class {
|
|
540
|
+
transporter;
|
|
541
|
+
MAX_EMAILS = 5;
|
|
542
|
+
WINDOW_MINUTES = 15;
|
|
543
|
+
BLOCK_HOURS = 1;
|
|
544
|
+
constructor() {
|
|
545
|
+
this.transporter = nodemailer.createTransport({
|
|
546
|
+
host: process.env.EMAIL_HOST || "smtp.postmarkapp.com",
|
|
547
|
+
port: process.env.EMAIL_PORT ? Number(process.env.EMAIL_PORT) : 587,
|
|
548
|
+
secure: (process.env.EMAIL_SECURE || "false") === "true",
|
|
549
|
+
auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD }
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
sign(payload, ttlSec = 60 * 60 * 24) {
|
|
553
|
+
return jwt3.sign(payload, process.env.EMAIL_JWT_SECRET, { expiresIn: ttlSec });
|
|
554
|
+
}
|
|
555
|
+
verify(token) {
|
|
556
|
+
return jwt3.verify(token, process.env.EMAIL_JWT_SECRET);
|
|
557
|
+
}
|
|
558
|
+
async send(to, subject, html) {
|
|
559
|
+
await this.transporter.sendMail({
|
|
560
|
+
from: process.env.EMAIL_FROM,
|
|
561
|
+
to,
|
|
562
|
+
subject,
|
|
563
|
+
html
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
canSend(lastEmailSent) {
|
|
567
|
+
const now = Date.now();
|
|
568
|
+
const windowStart = now - this.WINDOW_MINUTES * 60 * 1e3;
|
|
569
|
+
const emailsInWindow = (lastEmailSent || []).map((d) => new Date(d)).filter((d) => d.getTime() >= windowStart);
|
|
570
|
+
if (emailsInWindow.length >= this.MAX_EMAILS)
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
reason: "RATE_LIMIT",
|
|
574
|
+
waitMs: this.BLOCK_HOURS * 60 * 60 * 1e3
|
|
575
|
+
};
|
|
576
|
+
return { ok: true };
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/utils/cookie.ts
|
|
581
|
+
function cookieOpts(isRefresh = false) {
|
|
582
|
+
const maxAge = isRefresh ? config.cookies.refreshTtlMs : config.cookies.accessTtlMs;
|
|
583
|
+
const secure = process.env.NODE_ENV === "production" ? process.env.COOKIE_SECURE ?? true : false;
|
|
584
|
+
return {
|
|
585
|
+
httpOnly: true,
|
|
586
|
+
secure,
|
|
587
|
+
sameSite: "none",
|
|
588
|
+
domain: process.env.COOKIE_DOMAIN,
|
|
589
|
+
maxAge
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function clearOpts() {
|
|
593
|
+
const secure = process.env.NODE_ENV === "production" ? process.env.COOKIE_SECURE ?? true : false;
|
|
594
|
+
return {
|
|
595
|
+
domain: process.env.COOKIE_DOMAIN,
|
|
596
|
+
sameSite: "none",
|
|
597
|
+
secure
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/express/auth.routes.ts
|
|
602
|
+
function createAuthRouter(options = {}) {
|
|
603
|
+
if (options.config) {
|
|
604
|
+
configureAuthX(options.config);
|
|
605
|
+
}
|
|
606
|
+
const r = Router();
|
|
607
|
+
const email = new EmailService();
|
|
608
|
+
const authAdmin = new AuthAdminService();
|
|
609
|
+
r.use(express.json());
|
|
610
|
+
r.use(express.urlencoded({ extended: true }));
|
|
611
|
+
r.get(
|
|
612
|
+
"/healthz",
|
|
613
|
+
(_req, res) => res.json({ status: "ok", server: "org-server" })
|
|
614
|
+
);
|
|
615
|
+
r.post("/login", validateLogin, async (req, res) => {
|
|
616
|
+
const { email: emailAddress, password } = req.body || {};
|
|
617
|
+
try {
|
|
618
|
+
const user = await OrgUser.findOne({ email: emailAddress }).select("+password").lean();
|
|
619
|
+
if (!user) {
|
|
620
|
+
return res.status(400).json({
|
|
621
|
+
error: "Invalid email or password",
|
|
622
|
+
code: "INVALID_CREDENTIALS"
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (!user.emailVerified) {
|
|
626
|
+
return res.status(400).json({
|
|
627
|
+
error: "Please verify your email before logging in.",
|
|
628
|
+
code: "EMAIL_NOT_VERIFIED"
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const isPasswordValid = user.passwordHash ? await bcrypt2.compare(password, user.passwordHash) : false;
|
|
632
|
+
if (!isPasswordValid) {
|
|
633
|
+
return res.status(400).json({
|
|
634
|
+
error: "Invalid email or password",
|
|
635
|
+
code: "INVALID_CREDENTIALS"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const tokens = generateTokens(user);
|
|
639
|
+
setAuthCookies(res, tokens);
|
|
640
|
+
if (user.projectId) {
|
|
641
|
+
res.cookie(options.projectCookieName || "projectId", user.projectId, {
|
|
642
|
+
...cookieOpts(false),
|
|
643
|
+
httpOnly: true
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return res.json({
|
|
647
|
+
message: "Login successful",
|
|
648
|
+
user: toUserResponse(user)
|
|
649
|
+
});
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.error("Login error:", err);
|
|
652
|
+
return res.status(500).json({ error: "Internal server error" });
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
r.post("/signup", validateSignup, async (req, res) => {
|
|
656
|
+
const {
|
|
657
|
+
firstName,
|
|
658
|
+
lastName,
|
|
659
|
+
email: emailAddress,
|
|
660
|
+
password,
|
|
661
|
+
projectId,
|
|
662
|
+
metadata
|
|
663
|
+
} = req.body || {};
|
|
664
|
+
try {
|
|
665
|
+
const kcUser = await authAdmin.createUserInRealm({
|
|
666
|
+
username: emailAddress,
|
|
667
|
+
email: emailAddress,
|
|
668
|
+
firstName,
|
|
669
|
+
lastName,
|
|
670
|
+
projectId,
|
|
671
|
+
credentials: [{ type: "password", value: password, temporary: false }]
|
|
672
|
+
});
|
|
673
|
+
await authAdmin.assignRealmRole(kcUser.id, "platform_user");
|
|
674
|
+
const user = await OrgUser.findOneAndUpdate(
|
|
675
|
+
{ email: kcUser.email },
|
|
676
|
+
{
|
|
677
|
+
id: kcUser.id,
|
|
678
|
+
email: kcUser.email,
|
|
679
|
+
firstName,
|
|
680
|
+
lastName,
|
|
681
|
+
projectId,
|
|
682
|
+
metadata,
|
|
683
|
+
roles: ["platform_user"],
|
|
684
|
+
emailVerified: false
|
|
685
|
+
},
|
|
686
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
687
|
+
);
|
|
688
|
+
const emailResult = await sendRateLimitedEmail({
|
|
689
|
+
emailService: email,
|
|
690
|
+
user,
|
|
691
|
+
subject: "Verify your email",
|
|
692
|
+
html: buildVerificationTemplate(
|
|
693
|
+
email.sign({ userId: kcUser.id, email: kcUser.email }),
|
|
694
|
+
options
|
|
695
|
+
)
|
|
696
|
+
});
|
|
697
|
+
if (emailResult.rateLimited) {
|
|
698
|
+
return res.status(429).json({
|
|
699
|
+
ok: false,
|
|
700
|
+
error: "Too many verification emails sent. Please try again later.",
|
|
701
|
+
waitMs: emailResult.waitMs
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return res.json({
|
|
705
|
+
id: user.id,
|
|
706
|
+
email: user.email,
|
|
707
|
+
message: "Verification email sent. Please check your inbox."
|
|
708
|
+
});
|
|
709
|
+
} catch (err) {
|
|
710
|
+
return respondWithKeycloakError(res, err, "Signup failed");
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
r.get("/me", requireAuth(), (req, res) => {
|
|
714
|
+
return res.json(req.user || null);
|
|
715
|
+
});
|
|
716
|
+
r.post("/logout", async (_req, res) => {
|
|
717
|
+
res.clearCookie("access_token", clearOpts());
|
|
718
|
+
res.clearCookie("refresh_token", clearOpts());
|
|
719
|
+
res.json({ ok: true });
|
|
720
|
+
});
|
|
721
|
+
r.put("/:userId/metadata", requireAuth(), async (req, res) => {
|
|
722
|
+
const { userId } = req.params;
|
|
723
|
+
const { metadata } = req.body || {};
|
|
724
|
+
const user = await OrgUser.findOne({ id: userId });
|
|
725
|
+
if (!user)
|
|
726
|
+
return res.status(404).json({ ok: false, error: "User not found" });
|
|
727
|
+
const map = new Map(
|
|
728
|
+
(user.metadata || []).map((m) => [m.key, m.value])
|
|
729
|
+
);
|
|
730
|
+
for (const item of metadata || []) map.set(item.key, item.value);
|
|
731
|
+
user.metadata = Array.from(map.entries()).map(([key, value]) => ({
|
|
732
|
+
key,
|
|
733
|
+
value
|
|
734
|
+
}));
|
|
735
|
+
await user.save();
|
|
736
|
+
res.json({ ok: true, metadata: user.metadata });
|
|
737
|
+
});
|
|
738
|
+
r.get("/verify-email", async (req, res) => {
|
|
739
|
+
const token = String(req.query.token || "");
|
|
740
|
+
if (!token) {
|
|
741
|
+
return res.status(400).json({ error: "Verification token is required" });
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const payload = email.verify(token);
|
|
745
|
+
await authAdmin.updateUserEmailVerified(payload.userId, true);
|
|
746
|
+
await OrgUser.updateOne(
|
|
747
|
+
{ id: payload.userId },
|
|
748
|
+
{ $set: { emailVerified: true } }
|
|
749
|
+
);
|
|
750
|
+
res.json({ ok: true, message: "Email verified" });
|
|
751
|
+
} catch (err) {
|
|
752
|
+
res.status(400).json({ ok: false, error: err?.message || "Invalid token" });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
r.post(
|
|
756
|
+
"/resend-verification-email",
|
|
757
|
+
validateResendEmail,
|
|
758
|
+
async (req, res) => {
|
|
759
|
+
const user = await OrgUser.findOne({ email: req.body.email });
|
|
760
|
+
if (!user)
|
|
761
|
+
return res.status(404).json({ ok: false, error: "User not found" });
|
|
762
|
+
const verified = await authAdmin.isUserEmailVerified(user.id);
|
|
763
|
+
if (verified) {
|
|
764
|
+
return res.status(400).json({ ok: false, error: "Email is already verified" });
|
|
765
|
+
}
|
|
766
|
+
const token = email.sign({
|
|
767
|
+
email: user.email,
|
|
768
|
+
userId: user.id
|
|
769
|
+
});
|
|
770
|
+
const resendResult = await sendRateLimitedEmail({
|
|
771
|
+
emailService: email,
|
|
772
|
+
user,
|
|
773
|
+
subject: "Verify your email",
|
|
774
|
+
html: buildVerificationTemplate(token, options)
|
|
775
|
+
});
|
|
776
|
+
if (resendResult.rateLimited) {
|
|
777
|
+
return res.status(429).json({
|
|
778
|
+
ok: false,
|
|
779
|
+
error: "Too many verification emails sent. Please try again later.",
|
|
780
|
+
waitMs: resendResult.waitMs
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
res.json({ ok: true });
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
r.post("/forgot-password", validateResendEmail, async (req, res) => {
|
|
787
|
+
const user = await OrgUser.findOne({ email: req.body.email });
|
|
788
|
+
if (!user)
|
|
789
|
+
return res.status(404).json({ ok: false, error: "User not found" });
|
|
790
|
+
const resetToken = email.sign(
|
|
791
|
+
{
|
|
792
|
+
userId: user.id,
|
|
793
|
+
email: user.email,
|
|
794
|
+
firstName: user.firstName,
|
|
795
|
+
lastName: user.lastName
|
|
796
|
+
},
|
|
797
|
+
60 * 60
|
|
798
|
+
);
|
|
799
|
+
const resetResult = await sendRateLimitedEmail({
|
|
800
|
+
emailService: email,
|
|
801
|
+
user,
|
|
802
|
+
subject: "Reset password",
|
|
803
|
+
html: buildResetTemplate(resetToken, options)
|
|
804
|
+
});
|
|
805
|
+
if (resetResult.rateLimited) {
|
|
806
|
+
return res.status(429).json({
|
|
807
|
+
ok: false,
|
|
808
|
+
error: "Please wait before requesting another password reset email.",
|
|
809
|
+
waitMs: resetResult.waitMs
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
res.json({ ok: true, message: "Password reset email sent" });
|
|
813
|
+
});
|
|
814
|
+
r.post("/reset-password", validateResetPassword, async (req, res) => {
|
|
815
|
+
const { token, newPassword } = req.body || {};
|
|
816
|
+
try {
|
|
817
|
+
const payload = email.verify(token);
|
|
818
|
+
const user = await OrgUser.findOne({ keycloakId: payload.userId });
|
|
819
|
+
if (!user) {
|
|
820
|
+
return res.status(404).json({ ok: false, error: "User not found" });
|
|
821
|
+
}
|
|
822
|
+
if (user.lastPasswordReset && payload.iat * 1e3 < user.lastPasswordReset.getTime()) {
|
|
823
|
+
return res.status(400).json({
|
|
824
|
+
ok: false,
|
|
825
|
+
error: "This reset link has already been used. Please request a new one."
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
await authAdmin.updateUserPassword(payload.userId, newPassword);
|
|
829
|
+
user.lastPasswordReset = /* @__PURE__ */ new Date();
|
|
830
|
+
await user.save();
|
|
831
|
+
res.json({ ok: true, message: "Password updated successfully" });
|
|
832
|
+
} catch (err) {
|
|
833
|
+
res.status(400).json({ ok: false, error: err?.message || "Invalid or expired token" });
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
r.post(
|
|
837
|
+
"/send-invite",
|
|
838
|
+
requireAuth(),
|
|
839
|
+
validateSendInvite,
|
|
840
|
+
async (req, res) => {
|
|
841
|
+
const { email: emailAddress, role } = req.body || {};
|
|
842
|
+
const existingUser = await OrgUser.findOne({ email: emailAddress });
|
|
843
|
+
if (existingUser) {
|
|
844
|
+
return res.status(400).json({ ok: false, error: "User with this email already exists" });
|
|
845
|
+
}
|
|
846
|
+
const existingInvite = await Invite.findOne({
|
|
847
|
+
email: emailAddress,
|
|
848
|
+
isUsed: false,
|
|
849
|
+
isExpired: false
|
|
850
|
+
});
|
|
851
|
+
if (existingInvite) {
|
|
852
|
+
return res.status(400).json({
|
|
853
|
+
ok: false,
|
|
854
|
+
error: "An active invite already exists for this email"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const token = email.sign({
|
|
858
|
+
email: emailAddress,
|
|
859
|
+
role,
|
|
860
|
+
inviteId: randomUUID()
|
|
861
|
+
});
|
|
862
|
+
const invite = await Invite.create({
|
|
863
|
+
id: token,
|
|
864
|
+
email: emailAddress,
|
|
865
|
+
role,
|
|
866
|
+
invitedBy: req.user?.sub,
|
|
867
|
+
isUsed: false,
|
|
868
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3)
|
|
869
|
+
});
|
|
870
|
+
await email.send(
|
|
871
|
+
emailAddress,
|
|
872
|
+
"You are invited",
|
|
873
|
+
`<a href="${getFrontendBaseUrl(options)}/auth/accept-invite?token=${token}">Accept</a>`
|
|
874
|
+
);
|
|
875
|
+
res.json({
|
|
876
|
+
ok: true,
|
|
877
|
+
inviteId: invite.id,
|
|
878
|
+
email: invite.email,
|
|
879
|
+
role: invite.role,
|
|
880
|
+
expiresAt: invite.expiresAt
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
r.get("/accept-invite", async (req, res) => {
|
|
885
|
+
const inv = await Invite.findOne({ id: String(req.query.token) });
|
|
886
|
+
res.json({ ok: !!inv && !inv.isUsed && !inv.isExpired });
|
|
887
|
+
});
|
|
888
|
+
r.post("/accept-invite", async (req, res) => {
|
|
889
|
+
const { token, firstName, lastName, password, projectId } = req.body || {};
|
|
890
|
+
if (!token || !firstName || !lastName || !isPasswordStrong(password || "")) {
|
|
891
|
+
return res.status(400).json({ ok: false, error: "Invalid payload" });
|
|
892
|
+
}
|
|
893
|
+
const invite = await Invite.findOne({
|
|
894
|
+
id: token,
|
|
895
|
+
isUsed: false,
|
|
896
|
+
isExpired: false
|
|
897
|
+
});
|
|
898
|
+
if (!invite) {
|
|
899
|
+
return res.status(400).json({ ok: false, error: "Invitation not found or already used" });
|
|
900
|
+
}
|
|
901
|
+
if (invite.expiresAt && invite.expiresAt.getTime() < Date.now()) {
|
|
902
|
+
invite.isExpired = true;
|
|
903
|
+
await invite.save();
|
|
904
|
+
return res.status(400).json({ ok: false, error: "Invitation has expired" });
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const kcUser = await authAdmin.createUserInRealm({
|
|
908
|
+
username: invite.email,
|
|
909
|
+
email: invite.email,
|
|
910
|
+
firstName,
|
|
911
|
+
lastName,
|
|
912
|
+
projectId,
|
|
913
|
+
emailVerified: true,
|
|
914
|
+
credentials: [{ type: "password", value: password, temporary: false }]
|
|
915
|
+
});
|
|
916
|
+
await authAdmin.assignRealmRole(kcUser.id, invite.role);
|
|
917
|
+
await OrgUser.findOneAndUpdate(
|
|
918
|
+
{ email: invite.email },
|
|
919
|
+
{
|
|
920
|
+
id: kcUser.id,
|
|
921
|
+
email: invite.email,
|
|
922
|
+
firstName,
|
|
923
|
+
lastName,
|
|
924
|
+
roles: [invite.role],
|
|
925
|
+
emailVerified: true
|
|
926
|
+
},
|
|
927
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
928
|
+
);
|
|
929
|
+
invite.isUsed = true;
|
|
930
|
+
invite.usedAt = /* @__PURE__ */ new Date();
|
|
931
|
+
invite.usedBy = kcUser.id;
|
|
932
|
+
await invite.save();
|
|
933
|
+
res.json({
|
|
934
|
+
ok: true,
|
|
935
|
+
message: "Account created successfully.",
|
|
936
|
+
email: invite.email
|
|
937
|
+
});
|
|
938
|
+
} catch (err) {
|
|
939
|
+
res.status(400).json({
|
|
940
|
+
ok: false,
|
|
941
|
+
error: err?.response?.data?.error_description || err?.message || "Failed to create account"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
r.get("/invites", requireAuth(), async (_req, res) => {
|
|
946
|
+
const invites = await Invite.find().sort({ createdAt: -1 }).lean();
|
|
947
|
+
res.json(invites);
|
|
948
|
+
});
|
|
949
|
+
r.delete("/invites/:inviteId", requireAuth(), async (req, res) => {
|
|
950
|
+
await Invite.deleteOne({ id: req.params.inviteId });
|
|
951
|
+
res.json({ ok: true });
|
|
952
|
+
});
|
|
953
|
+
r.get("/get-user-by-email", async (req, res) => {
|
|
954
|
+
const user = await OrgUser.findOne({ email: req.query.email }).lean();
|
|
955
|
+
res.json(user || null);
|
|
956
|
+
});
|
|
957
|
+
r.get("/google", async (_req, res) => {
|
|
958
|
+
res.json({ url: "/auth/google/callback?code=demo" });
|
|
959
|
+
});
|
|
960
|
+
r.get("/google/callback", async (_req, res) => {
|
|
961
|
+
res.cookie(
|
|
962
|
+
"access_token",
|
|
963
|
+
"ACCESS.TOKEN.PLACEHOLDER",
|
|
964
|
+
cookieOpts(false)
|
|
965
|
+
);
|
|
966
|
+
res.redirect("/");
|
|
967
|
+
});
|
|
968
|
+
r.get("/get-users", async (req, res) => {
|
|
969
|
+
const user = await OrgUser.find({ projectId: req.query.projectId }).lean();
|
|
970
|
+
res.json(user || null);
|
|
971
|
+
});
|
|
972
|
+
return r;
|
|
973
|
+
}
|
|
974
|
+
function setAuthCookies(res, tokens) {
|
|
975
|
+
if (tokens?.access_token) {
|
|
976
|
+
res.cookie("access_token", tokens.access_token, {
|
|
977
|
+
httpOnly: true,
|
|
978
|
+
secure: false,
|
|
979
|
+
sameSite: "lax",
|
|
980
|
+
maxAge: 24 * 60 * 60 * 1e3,
|
|
981
|
+
// 24 hours
|
|
982
|
+
path: "/"
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
if (tokens?.refresh_token) {
|
|
986
|
+
res.cookie("refresh_token", tokens.refresh_token, {
|
|
987
|
+
httpOnly: true,
|
|
988
|
+
secure: false,
|
|
989
|
+
sameSite: "lax",
|
|
990
|
+
maxAge: 24 * 60 * 60 * 1e3,
|
|
991
|
+
// 24 hours
|
|
992
|
+
path: "/"
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function toUserResponse(user) {
|
|
997
|
+
if (!user) return null;
|
|
998
|
+
return {
|
|
999
|
+
sub: user.id || user.keycloakId,
|
|
1000
|
+
email: user.email,
|
|
1001
|
+
firstName: user.firstName,
|
|
1002
|
+
lastName: user.lastName,
|
|
1003
|
+
projectId: user.projectId,
|
|
1004
|
+
metadata: user.metadata,
|
|
1005
|
+
roles: user.roles
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function respondWithKeycloakError(res, err, fallback, status = 400) {
|
|
1009
|
+
const description = err?.response?.data?.error_description || err?.response?.data?.errorMessage || err?.message || fallback;
|
|
1010
|
+
return res.status(status).json({ ok: false, error: description });
|
|
1011
|
+
}
|
|
1012
|
+
function buildVerificationTemplate(token, options) {
|
|
1013
|
+
return `<a href="${getFrontendBaseUrl(options)}/auth/verify-email?token=${token}">Verify</a>`;
|
|
1014
|
+
}
|
|
1015
|
+
function buildResetTemplate(token, options) {
|
|
1016
|
+
return `<a href="${getFrontendBaseUrl(options)}/auth/reset-password?token=${token}">Reset</a>`;
|
|
1017
|
+
}
|
|
1018
|
+
function getFrontendBaseUrl(options) {
|
|
1019
|
+
if (options.frontendBaseUrl)
|
|
1020
|
+
return options.frontendBaseUrl.replace(/\/$/, "");
|
|
1021
|
+
const domain = process.env.ORG_DOMAIN?.replace(/\/$/, "");
|
|
1022
|
+
if (!domain) return "";
|
|
1023
|
+
return domain.startsWith("http") ? domain : `https://${domain}`;
|
|
1024
|
+
}
|
|
1025
|
+
async function sendRateLimitedEmail({
|
|
1026
|
+
emailService,
|
|
1027
|
+
user,
|
|
1028
|
+
subject,
|
|
1029
|
+
html
|
|
1030
|
+
}) {
|
|
1031
|
+
const can = emailService.canSend(user?.lastEmailSent || []);
|
|
1032
|
+
if (!can.ok) {
|
|
1033
|
+
return { rateLimited: true, waitMs: can.waitMs };
|
|
1034
|
+
}
|
|
1035
|
+
await emailService.send(user.email, subject, html);
|
|
1036
|
+
user.lastEmailSent = [...user.lastEmailSent || [], /* @__PURE__ */ new Date()];
|
|
1037
|
+
await user.save();
|
|
1038
|
+
return { rateLimited: false };
|
|
1039
|
+
}
|
|
1040
|
+
function generateTokens(user) {
|
|
1041
|
+
const accessToken = jwt4.sign(
|
|
1042
|
+
{
|
|
1043
|
+
sub: user.id.toString(),
|
|
1044
|
+
email: user.email,
|
|
1045
|
+
roles: user.roles || [],
|
|
1046
|
+
type: "user"
|
|
1047
|
+
},
|
|
1048
|
+
process.env.JWT_SECRET,
|
|
1049
|
+
{ expiresIn: "1h" }
|
|
1050
|
+
);
|
|
1051
|
+
const refreshToken = jwt4.sign(
|
|
1052
|
+
{ sub: user._id.toString() },
|
|
1053
|
+
process.env.JWT_SECRET,
|
|
1054
|
+
{ expiresIn: "30d" }
|
|
1055
|
+
);
|
|
1056
|
+
return { access_token: accessToken, refresh_token: refreshToken };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/express/dashboards.routes.ts
|
|
1060
|
+
import express2, { Router as Router2 } from "express";
|
|
1061
|
+
function createDashboardRouter(options) {
|
|
1062
|
+
const r = Router2();
|
|
1063
|
+
const kc = new AuthAdminService();
|
|
1064
|
+
r.use(express2.json());
|
|
1065
|
+
r.post("/", requireAuth(), async (req, res, next) => {
|
|
1066
|
+
try {
|
|
1067
|
+
const { slug, isPublic, authFlow, orgDomain } = req.body || {};
|
|
1068
|
+
const redirectUris = [`https://${slug}.${orgDomain}/*`];
|
|
1069
|
+
const created = await kc.createClient(slug, redirectUris, !!isPublic);
|
|
1070
|
+
if (authFlow || isPublic != null) {
|
|
1071
|
+
await kc.updateClient(created.id, {
|
|
1072
|
+
authenticationFlowBindingOverrides: authFlow ? { browser: authFlow } : void 0,
|
|
1073
|
+
registrationAllowed: !!isPublic
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
res.json({ clientId: created.clientId });
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
next(e);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
return r;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/express/email.routes.ts
|
|
1085
|
+
import { Router as Router3 } from "express";
|
|
1086
|
+
function createEmailRouter(options) {
|
|
1087
|
+
const r = Router3();
|
|
1088
|
+
r.get(
|
|
1089
|
+
"/verify",
|
|
1090
|
+
(req, res) => res.json({ ok: true, token: req.query.token })
|
|
1091
|
+
);
|
|
1092
|
+
return r;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/express/projects.routes.ts
|
|
1096
|
+
import { Router as Router4 } from "express";
|
|
1097
|
+
|
|
1098
|
+
// src/services/projects.service.ts
|
|
1099
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1100
|
+
|
|
1101
|
+
// src/models/moduleConnection.model.ts
|
|
1102
|
+
import mongoose5 from "mongoose";
|
|
1103
|
+
var ModuleItemSchema = new mongoose5.Schema(
|
|
1104
|
+
{ id: { type: String, required: true } },
|
|
1105
|
+
{ _id: false }
|
|
1106
|
+
);
|
|
1107
|
+
var ModuleConnectionSchema = new mongoose5.Schema(
|
|
1108
|
+
{
|
|
1109
|
+
projectId: { type: String, required: true, index: true },
|
|
1110
|
+
modules: {
|
|
1111
|
+
data: { type: [ModuleItemSchema], default: [] },
|
|
1112
|
+
integration: { type: [ModuleItemSchema], default: [] },
|
|
1113
|
+
storage: { type: [ModuleItemSchema], default: [] }
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{ timestamps: true, collection: "module_connection" }
|
|
1117
|
+
);
|
|
1118
|
+
var ModuleConnection = mongoose5.model(
|
|
1119
|
+
"ModuleConnection",
|
|
1120
|
+
ModuleConnectionSchema
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
// src/models/project.model.ts
|
|
1124
|
+
import mongoose6 from "mongoose";
|
|
1125
|
+
var ProjectSchema = new mongoose6.Schema(
|
|
1126
|
+
{
|
|
1127
|
+
_id: { type: String, required: true },
|
|
1128
|
+
org_id: { type: String, required: true, index: true },
|
|
1129
|
+
name: { type: String, required: true },
|
|
1130
|
+
description: { type: String },
|
|
1131
|
+
secret: { type: String, required: true }
|
|
1132
|
+
},
|
|
1133
|
+
{ timestamps: true, collection: "projects" }
|
|
1134
|
+
);
|
|
1135
|
+
var Project = mongoose6.model("Project", ProjectSchema);
|
|
1136
|
+
|
|
1137
|
+
// src/services/projects.service.ts
|
|
1138
|
+
var ProjectsService = class {
|
|
1139
|
+
async create(org_id, name, description) {
|
|
1140
|
+
const _id = randomUUID2();
|
|
1141
|
+
const secret = randomUUID2();
|
|
1142
|
+
const p = await Project.create({ _id, org_id, name, description, secret });
|
|
1143
|
+
await ModuleConnection.create({
|
|
1144
|
+
projectId: _id,
|
|
1145
|
+
modules: { data: [], integration: [], storage: [] }
|
|
1146
|
+
});
|
|
1147
|
+
return p.toObject();
|
|
1148
|
+
}
|
|
1149
|
+
async list(org_id) {
|
|
1150
|
+
return Project.find({ org_id }).lean();
|
|
1151
|
+
}
|
|
1152
|
+
async get(org_id, id) {
|
|
1153
|
+
return Project.findOne({ org_id, _id: id }).lean();
|
|
1154
|
+
}
|
|
1155
|
+
async update(org_id, id, patch) {
|
|
1156
|
+
return Project.findOneAndUpdate(
|
|
1157
|
+
{ org_id, _id: id },
|
|
1158
|
+
{ $set: patch },
|
|
1159
|
+
{ new: true }
|
|
1160
|
+
).lean();
|
|
1161
|
+
}
|
|
1162
|
+
async remove(org_id, id) {
|
|
1163
|
+
await Project.deleteOne({ org_id, _id: id });
|
|
1164
|
+
await ModuleConnection.deleteMany({ projectId: id });
|
|
1165
|
+
return { ok: true };
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// src/express/projects.routes.ts
|
|
1170
|
+
function createProjectsRouter(options) {
|
|
1171
|
+
const r = Router4();
|
|
1172
|
+
const svc = new ProjectsService();
|
|
1173
|
+
r.post("/create", requireAuth(), async (req, res) => {
|
|
1174
|
+
const { org_id, name, description } = req.body || {};
|
|
1175
|
+
const p = await svc.create(org_id, name, description);
|
|
1176
|
+
res.json(p);
|
|
1177
|
+
});
|
|
1178
|
+
r.get("/:org_id", requireAuth(), async (req, res) => {
|
|
1179
|
+
res.json(await svc.list(req.params.org_id));
|
|
1180
|
+
});
|
|
1181
|
+
r.get("/:org_id/:id", requireAuth(), async (req, res) => {
|
|
1182
|
+
res.json(await svc.get(req.params.org_id, req.params.id));
|
|
1183
|
+
});
|
|
1184
|
+
r.put("/:org_id/:id", requireAuth(), async (req, res) => {
|
|
1185
|
+
res.json(
|
|
1186
|
+
await svc.update(req.params.org_id, req.params.id, req.body || {})
|
|
1187
|
+
);
|
|
1188
|
+
});
|
|
1189
|
+
r.delete("/:org_id/:id", requireAuth(), async (req, res) => {
|
|
1190
|
+
res.json(await svc.remove(req.params.org_id, req.params.id));
|
|
1191
|
+
});
|
|
1192
|
+
return r;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/express/admin/admin.routes.ts
|
|
1196
|
+
import bcrypt3 from "bcryptjs";
|
|
1197
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1198
|
+
import express3, { Router as Router5 } from "express";
|
|
1199
|
+
|
|
1200
|
+
// src/core/utils.ts
|
|
1201
|
+
function hasRole(session, role) {
|
|
1202
|
+
if (!session || !session.roles) return false;
|
|
1203
|
+
return session.roles.includes(role);
|
|
1204
|
+
}
|
|
1205
|
+
function hasAnyRole(session, roles) {
|
|
1206
|
+
if (!session || !session.roles || !Array.isArray(roles) || roles.length === 0) {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
return roles.some((role) => session.roles.includes(role));
|
|
1210
|
+
}
|
|
1211
|
+
function hasAllRoles(session, roles) {
|
|
1212
|
+
if (!session || !session.roles || !Array.isArray(roles) || roles.length === 0) {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
return roles.every((role) => session.roles.includes(role));
|
|
1216
|
+
}
|
|
1217
|
+
function hasPermission(session, permission) {
|
|
1218
|
+
if (!session || !session.permissions) return false;
|
|
1219
|
+
return session.permissions.includes(permission);
|
|
1220
|
+
}
|
|
1221
|
+
function hasAnyPermission(session, permissions) {
|
|
1222
|
+
if (!session || !session.permissions || !Array.isArray(permissions) || permissions.length === 0) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
return permissions.some((perm) => session.permissions.includes(perm));
|
|
1226
|
+
}
|
|
1227
|
+
function hasAllPermissions(session, permissions) {
|
|
1228
|
+
if (!session || !session.permissions || !Array.isArray(permissions) || permissions.length === 0) {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
return permissions.every((perm) => session.permissions.includes(perm));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/middlewares/requireRole.ts
|
|
1235
|
+
function requireRole(...roles) {
|
|
1236
|
+
return (req, res, next) => {
|
|
1237
|
+
const user = req.user;
|
|
1238
|
+
if (!user) {
|
|
1239
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
1240
|
+
}
|
|
1241
|
+
if (!roles || roles.length === 0) {
|
|
1242
|
+
return next();
|
|
1243
|
+
}
|
|
1244
|
+
if (!hasAnyRole(user, roles)) {
|
|
1245
|
+
return res.status(403).json({
|
|
1246
|
+
error: `Requires one of roles: ${roles.join(", ")}`,
|
|
1247
|
+
required: roles,
|
|
1248
|
+
userRoles: user.roles
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
next();
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/models/permissions.model.ts
|
|
1256
|
+
import mongoose7, { Schema as Schema3 } from "mongoose";
|
|
1257
|
+
var PermissionsSchema = new Schema3(
|
|
1258
|
+
{
|
|
1259
|
+
id: { type: String, required: true, index: true },
|
|
1260
|
+
orgId: { type: String, default: null, index: true },
|
|
1261
|
+
key: { type: String, required: true },
|
|
1262
|
+
type: { type: String, required: true },
|
|
1263
|
+
apiId: { type: String, required: false },
|
|
1264
|
+
description: { type: String },
|
|
1265
|
+
isInternal: { type: Boolean, default: false }
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
timestamps: true
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
PermissionsSchema.index({ orgId: 1, key: 1 }, { unique: true });
|
|
1272
|
+
var PermissionsModel = mongoose7.model(
|
|
1273
|
+
"Permissions",
|
|
1274
|
+
PermissionsSchema,
|
|
1275
|
+
"permissions"
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
// src/express/admin/admin.routes.ts
|
|
1279
|
+
function resolveOrgId(req) {
|
|
1280
|
+
const user = req.user || {};
|
|
1281
|
+
const fromUser = user.orgId || user.org_id || null;
|
|
1282
|
+
const fromQuery = req.query.orgId || null;
|
|
1283
|
+
const fromBody = req.body && req.body.orgId || null;
|
|
1284
|
+
return fromQuery || fromBody || fromUser;
|
|
1285
|
+
}
|
|
1286
|
+
function resolveProjectId(req) {
|
|
1287
|
+
const user = req.user || {};
|
|
1288
|
+
const fromUser = user.projectId || null;
|
|
1289
|
+
const fromQuery = req.query.projectId || null;
|
|
1290
|
+
const fromBody = req.body && req.body.projectId || null;
|
|
1291
|
+
return fromQuery || fromBody || fromUser;
|
|
1292
|
+
}
|
|
1293
|
+
function createAdminRouter(_options = {}) {
|
|
1294
|
+
const r = Router5();
|
|
1295
|
+
r.use(express3.json());
|
|
1296
|
+
r.use(express3.urlencoded({ extended: true }));
|
|
1297
|
+
const adminGuards = [requireAuth(), requireRole("platform_admin")];
|
|
1298
|
+
r.post(
|
|
1299
|
+
"/users",
|
|
1300
|
+
...adminGuards,
|
|
1301
|
+
async (req, res) => {
|
|
1302
|
+
const {
|
|
1303
|
+
firstName,
|
|
1304
|
+
lastName,
|
|
1305
|
+
email: emailAddress,
|
|
1306
|
+
password,
|
|
1307
|
+
emailVerified = false,
|
|
1308
|
+
roles = []
|
|
1309
|
+
} = req.body || {};
|
|
1310
|
+
const projectId = resolveProjectId(req);
|
|
1311
|
+
try {
|
|
1312
|
+
const hashedPassword = password ? await bcrypt3.hash(password, 10) : void 0;
|
|
1313
|
+
const user = await OrgUser.create({
|
|
1314
|
+
id: randomUUID3(),
|
|
1315
|
+
email: emailAddress,
|
|
1316
|
+
orgId: process.env.ORG_ID,
|
|
1317
|
+
firstName,
|
|
1318
|
+
lastName,
|
|
1319
|
+
projectId,
|
|
1320
|
+
emailVerified,
|
|
1321
|
+
metadata: [],
|
|
1322
|
+
passwordHash: hashedPassword,
|
|
1323
|
+
roles
|
|
1324
|
+
});
|
|
1325
|
+
return res.json({
|
|
1326
|
+
id: user.id,
|
|
1327
|
+
email: user.email,
|
|
1328
|
+
message: "Verification email sent. Please check your inbox."
|
|
1329
|
+
});
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
console.error("Create user error:", err);
|
|
1332
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
);
|
|
1336
|
+
r.delete(
|
|
1337
|
+
"/users",
|
|
1338
|
+
...adminGuards,
|
|
1339
|
+
async (req, res) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const userId = req?.body?.id || req?.query?.id;
|
|
1342
|
+
if (!userId) {
|
|
1343
|
+
return res.status(400).json({
|
|
1344
|
+
error: "VALIDATION_ERROR",
|
|
1345
|
+
message: "UserId is required (send in body.id or ?id=...)"
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
const deleted = await OrgUser.findOneAndDelete({ id: userId }).exec();
|
|
1349
|
+
if (!deleted) {
|
|
1350
|
+
return res.status(404).json({
|
|
1351
|
+
error: "NOT_FOUND",
|
|
1352
|
+
message: "User not found or already deleted"
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return res.status(200).json({
|
|
1356
|
+
ok: true,
|
|
1357
|
+
message: "User deleted successfully",
|
|
1358
|
+
deletedUser: {
|
|
1359
|
+
id: deleted.id,
|
|
1360
|
+
firstName: deleted.firstName,
|
|
1361
|
+
email: deleted.email,
|
|
1362
|
+
orgId: deleted.orgId
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
console.error("Delete user error:", err);
|
|
1367
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
r.put(
|
|
1372
|
+
"/users/:id",
|
|
1373
|
+
...adminGuards,
|
|
1374
|
+
async (req, res) => {
|
|
1375
|
+
const userId = req.params.id;
|
|
1376
|
+
const {
|
|
1377
|
+
firstName,
|
|
1378
|
+
lastName,
|
|
1379
|
+
email: emailAddress,
|
|
1380
|
+
password,
|
|
1381
|
+
emailVerified,
|
|
1382
|
+
roles
|
|
1383
|
+
} = req.body || {};
|
|
1384
|
+
try {
|
|
1385
|
+
const existingUser = await OrgUser.findOne({
|
|
1386
|
+
id: userId,
|
|
1387
|
+
orgId: process.env.ORG_ID
|
|
1388
|
+
});
|
|
1389
|
+
if (!existingUser) {
|
|
1390
|
+
return res.status(404).json({ error: "USER_NOT_FOUND" });
|
|
1391
|
+
}
|
|
1392
|
+
if (firstName !== void 0) existingUser.firstName = firstName;
|
|
1393
|
+
if (lastName !== void 0) existingUser.lastName = lastName;
|
|
1394
|
+
if (emailAddress !== void 0) existingUser.email = emailAddress;
|
|
1395
|
+
if (emailVerified !== void 0)
|
|
1396
|
+
existingUser.emailVerified = emailVerified;
|
|
1397
|
+
if (roles !== void 0) existingUser.roles = roles;
|
|
1398
|
+
if (password) {
|
|
1399
|
+
existingUser.passwordHash = await bcrypt3.hash(password, 10);
|
|
1400
|
+
}
|
|
1401
|
+
await existingUser.save();
|
|
1402
|
+
return res.json({
|
|
1403
|
+
id: existingUser.id,
|
|
1404
|
+
email: existingUser.email,
|
|
1405
|
+
message: "User updated successfully."
|
|
1406
|
+
});
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
console.error("Update user error:", err);
|
|
1409
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
);
|
|
1413
|
+
r.get(
|
|
1414
|
+
"/permissions",
|
|
1415
|
+
...adminGuards,
|
|
1416
|
+
async (req, res) => {
|
|
1417
|
+
try {
|
|
1418
|
+
const orgId = resolveOrgId(req);
|
|
1419
|
+
const filter = {};
|
|
1420
|
+
if (orgId !== null) {
|
|
1421
|
+
filter.orgId = orgId;
|
|
1422
|
+
} else {
|
|
1423
|
+
filter.orgId = null;
|
|
1424
|
+
}
|
|
1425
|
+
const items = await PermissionsModel.find(filter).lean().exec();
|
|
1426
|
+
return res.json(items);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
r.post(
|
|
1433
|
+
"/permissions",
|
|
1434
|
+
...adminGuards,
|
|
1435
|
+
async (req, res) => {
|
|
1436
|
+
try {
|
|
1437
|
+
const orgId = resolveOrgId(req);
|
|
1438
|
+
const {
|
|
1439
|
+
key,
|
|
1440
|
+
type,
|
|
1441
|
+
apiId,
|
|
1442
|
+
description,
|
|
1443
|
+
isInternal = false
|
|
1444
|
+
} = req.body || {};
|
|
1445
|
+
if (!key || !type) {
|
|
1446
|
+
return res.status(400).json({
|
|
1447
|
+
error: "VALIDATION_ERROR",
|
|
1448
|
+
message: "permission key, and permission type are required"
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
const id = randomUUID3();
|
|
1452
|
+
const permission = await PermissionsModel.create({
|
|
1453
|
+
id,
|
|
1454
|
+
orgId: orgId ?? null,
|
|
1455
|
+
key,
|
|
1456
|
+
type,
|
|
1457
|
+
apiId,
|
|
1458
|
+
description,
|
|
1459
|
+
isInternal: !!isInternal
|
|
1460
|
+
});
|
|
1461
|
+
await RolePermissionModel.findOneAndUpdate(
|
|
1462
|
+
{ orgId: orgId ?? null, role: "platform_admin" },
|
|
1463
|
+
{ $addToSet: { permissions: key } },
|
|
1464
|
+
{ upsert: true, new: true }
|
|
1465
|
+
).exec();
|
|
1466
|
+
return res.status(201).json(permission);
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
if (err && err.code === 11e3) {
|
|
1469
|
+
return res.status(409).json({
|
|
1470
|
+
error: "DUPLICATE_PERMISSION",
|
|
1471
|
+
message: "Permission key already exists for this org"
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
r.put(
|
|
1479
|
+
"/permissions/:id",
|
|
1480
|
+
...adminGuards,
|
|
1481
|
+
async (req, res) => {
|
|
1482
|
+
try {
|
|
1483
|
+
const orgId = resolveOrgId(req);
|
|
1484
|
+
const permissionId = req.params.id;
|
|
1485
|
+
const { key, type, apiId, description, isInternal } = req.body || {};
|
|
1486
|
+
const existing = await PermissionsModel.findOne({
|
|
1487
|
+
id: permissionId,
|
|
1488
|
+
orgId: orgId ?? null
|
|
1489
|
+
});
|
|
1490
|
+
if (!existing) {
|
|
1491
|
+
return res.status(404).json({
|
|
1492
|
+
error: "NOT_FOUND",
|
|
1493
|
+
message: "Permission does not exist"
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
const oldKey = existing.key;
|
|
1497
|
+
if (key !== void 0) existing.key = key;
|
|
1498
|
+
if (type !== void 0) existing.type = type;
|
|
1499
|
+
if (apiId !== void 0) existing.apiId = apiId;
|
|
1500
|
+
if (description !== void 0) existing.description = description;
|
|
1501
|
+
if (isInternal !== void 0) existing.isInternal = !!isInternal;
|
|
1502
|
+
await existing.save();
|
|
1503
|
+
if (oldKey !== key) {
|
|
1504
|
+
await RolePermissionModel.updateMany(
|
|
1505
|
+
{
|
|
1506
|
+
orgId: orgId ?? null,
|
|
1507
|
+
permissions: oldKey
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
$pull: { permissions: oldKey }
|
|
1511
|
+
}
|
|
1512
|
+
);
|
|
1513
|
+
await RolePermissionModel.updateMany(
|
|
1514
|
+
{
|
|
1515
|
+
orgId: orgId ?? null
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
$addToSet: { permissions: key }
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
return res.json(existing);
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
if (err && err.code === 11e3) {
|
|
1525
|
+
return res.status(409).json({
|
|
1526
|
+
error: "DUPLICATE_PERMISSION",
|
|
1527
|
+
message: "Permission key already exists for this org"
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
console.error("Update permission error:", err);
|
|
1531
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
r.delete(
|
|
1536
|
+
"/permissions",
|
|
1537
|
+
...adminGuards,
|
|
1538
|
+
async (req, res) => {
|
|
1539
|
+
try {
|
|
1540
|
+
const permissionId = req?.body?.id || req?.query?.id;
|
|
1541
|
+
if (!permissionId) {
|
|
1542
|
+
return res.status(400).json({
|
|
1543
|
+
error: "VALIDATION_ERROR",
|
|
1544
|
+
message: "Permission id is required (send in body.id or ?id=...)"
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
const existing = await PermissionsModel.findOne({ id: permissionId });
|
|
1548
|
+
if (!existing) {
|
|
1549
|
+
return res.status(404).json({
|
|
1550
|
+
error: "NOT_FOUND",
|
|
1551
|
+
message: "Permission not found or already deleted"
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
const { key, orgId } = existing;
|
|
1555
|
+
await PermissionsModel.deleteOne({ id: permissionId });
|
|
1556
|
+
await RolePermissionModel.updateMany(
|
|
1557
|
+
{ orgId: orgId ?? null },
|
|
1558
|
+
{ $pull: { permissions: key } }
|
|
1559
|
+
);
|
|
1560
|
+
return res.status(200).json({
|
|
1561
|
+
ok: true,
|
|
1562
|
+
message: "Permission deleted successfully",
|
|
1563
|
+
deletedPermission: {
|
|
1564
|
+
id: existing.id,
|
|
1565
|
+
key: existing.key,
|
|
1566
|
+
type: existing.type,
|
|
1567
|
+
apiId: existing.apiId,
|
|
1568
|
+
description: existing.description,
|
|
1569
|
+
isInternal: existing.isInternal,
|
|
1570
|
+
orgId: existing.orgId
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
console.error("Delete permission error:", err);
|
|
1575
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
r.get(
|
|
1580
|
+
"/roles",
|
|
1581
|
+
...adminGuards,
|
|
1582
|
+
async (req, res) => {
|
|
1583
|
+
try {
|
|
1584
|
+
const orgId = resolveOrgId(req);
|
|
1585
|
+
const filter = {};
|
|
1586
|
+
if (orgId !== null) {
|
|
1587
|
+
filter.orgId = orgId;
|
|
1588
|
+
} else {
|
|
1589
|
+
filter.orgId = null;
|
|
1590
|
+
}
|
|
1591
|
+
const roles = await RolePermissionModel.find(filter).lean().exec();
|
|
1592
|
+
return res.json(roles);
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
r.post(
|
|
1599
|
+
"/roles",
|
|
1600
|
+
...adminGuards,
|
|
1601
|
+
async (req, res) => {
|
|
1602
|
+
try {
|
|
1603
|
+
const orgId = resolveOrgId(req);
|
|
1604
|
+
const { role, permissions } = req.body || {};
|
|
1605
|
+
if (!role || !Array.isArray(permissions)) {
|
|
1606
|
+
return res.status(400).json({
|
|
1607
|
+
error: "VALIDATION_ERROR",
|
|
1608
|
+
message: "role and permissions[] are required"
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
const id = randomUUID3();
|
|
1612
|
+
const doc = await RolePermissionModel.findOneAndUpdate(
|
|
1613
|
+
{ orgId: orgId ?? null, role },
|
|
1614
|
+
{ $set: { permissions } },
|
|
1615
|
+
{ upsert: true, new: true }
|
|
1616
|
+
).exec();
|
|
1617
|
+
return res.status(200).json(doc);
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
r.put(
|
|
1624
|
+
"/roles/:id",
|
|
1625
|
+
...adminGuards,
|
|
1626
|
+
async (req, res) => {
|
|
1627
|
+
try {
|
|
1628
|
+
const orgId = resolveOrgId(req);
|
|
1629
|
+
const roleId = req.params.id;
|
|
1630
|
+
const { role: newRoleName, permissions } = req.body || {};
|
|
1631
|
+
if (!newRoleName || !Array.isArray(permissions)) {
|
|
1632
|
+
return res.status(400).json({
|
|
1633
|
+
error: "VALIDATION_ERROR",
|
|
1634
|
+
message: "role and permissions are required"
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
const existing = await RolePermissionModel.findById(roleId);
|
|
1638
|
+
if (!existing) {
|
|
1639
|
+
return res.status(404).json({
|
|
1640
|
+
error: "ROLE_NOT_FOUND",
|
|
1641
|
+
message: "Role does not exist"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const oldRoleName = existing.role;
|
|
1645
|
+
existing.role = newRoleName;
|
|
1646
|
+
existing.permissions = permissions;
|
|
1647
|
+
await existing.save();
|
|
1648
|
+
if (oldRoleName !== newRoleName) {
|
|
1649
|
+
await OrgUser.updateMany(
|
|
1650
|
+
{
|
|
1651
|
+
orgId: orgId ?? null,
|
|
1652
|
+
roles: oldRoleName
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
$pull: { roles: oldRoleName }
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
await OrgUser.updateMany(
|
|
1659
|
+
{
|
|
1660
|
+
orgId: orgId ?? null,
|
|
1661
|
+
roles: { $ne: newRoleName }
|
|
1662
|
+
// avoid duplicates
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
$addToSet: { roles: newRoleName }
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
return res.status(200).json(existing);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
console.error("Update role error:", err);
|
|
1672
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
);
|
|
1676
|
+
r.delete(
|
|
1677
|
+
"/roles",
|
|
1678
|
+
...adminGuards,
|
|
1679
|
+
async (req, res) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const roleId = req?.body?.id || req?.query?.id;
|
|
1682
|
+
if (!roleId) {
|
|
1683
|
+
return res.status(400).json({
|
|
1684
|
+
error: "VALIDATION_ERROR",
|
|
1685
|
+
message: "Role _id is required (send in body.id or ?id=...)"
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
if (!/^[0-9a-fA-F]{24}$/.test(roleId)) {
|
|
1689
|
+
return res.status(400).json({
|
|
1690
|
+
error: "VALIDATION_ERROR",
|
|
1691
|
+
message: "Invalid role _id format"
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
const deleted = await RolePermissionModel.findByIdAndDelete(roleId).exec();
|
|
1695
|
+
if (!deleted) {
|
|
1696
|
+
return res.status(404).json({
|
|
1697
|
+
error: "NOT_FOUND",
|
|
1698
|
+
message: "Role not found or already deleted"
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return res.status(200).json({
|
|
1702
|
+
ok: true,
|
|
1703
|
+
message: "Role deleted successfully",
|
|
1704
|
+
deletedRole: {
|
|
1705
|
+
_id: deleted._id,
|
|
1706
|
+
role: deleted.role,
|
|
1707
|
+
orgId: deleted.orgId
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
console.error("Delete role error:", err);
|
|
1712
|
+
return res.status(500).json({ error: "INTERNAL_ERROR" });
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
);
|
|
1716
|
+
return r;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/nest/index.ts
|
|
1720
|
+
var nest = {
|
|
1721
|
+
mountAuthRouter(app, options) {
|
|
1722
|
+
const httpAdapter = app.getHttpAdapter().getInstance();
|
|
1723
|
+
const authRouter = createAuthRouter(options.routerOptions || {});
|
|
1724
|
+
const authPath = options.authBasePath || "/auth";
|
|
1725
|
+
httpAdapter.use(authPath, authRouter);
|
|
1726
|
+
const adminRouter = createAdminRouter(options);
|
|
1727
|
+
const adminPath = options.adminBasePath || "/admin";
|
|
1728
|
+
httpAdapter.use(adminPath, adminRouter);
|
|
1729
|
+
const dashboardRouter = createDashboardRouter(options);
|
|
1730
|
+
const dashboardPath = options.dashboardBasePath || "/dashboards";
|
|
1731
|
+
httpAdapter.use(dashboardPath, dashboardRouter);
|
|
1732
|
+
const emailRouter = createEmailRouter(options);
|
|
1733
|
+
const emailPath = options.emailBasePath || "/email";
|
|
1734
|
+
httpAdapter.use(emailPath, emailRouter);
|
|
1735
|
+
const projectsRouter = createProjectsRouter(options);
|
|
1736
|
+
const projectsPath = options.projectsBasePath || "/projects";
|
|
1737
|
+
httpAdapter.use(projectsPath, projectsRouter);
|
|
1738
|
+
return {
|
|
1739
|
+
authRouter,
|
|
1740
|
+
dashboardRouter,
|
|
1741
|
+
emailRouter,
|
|
1742
|
+
projectsRouter,
|
|
1743
|
+
adminRouter
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
// src/middlewares/permission.middleware.ts
|
|
1749
|
+
function requirePermission(permissionKey) {
|
|
1750
|
+
return async (req, res, next) => {
|
|
1751
|
+
try {
|
|
1752
|
+
console.log("INSIDE PERMISSION MIDDLEWARE", permissionKey);
|
|
1753
|
+
const user = req.user;
|
|
1754
|
+
if (!user) {
|
|
1755
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
1756
|
+
}
|
|
1757
|
+
const roles = Array.isArray(user.roles) ? user.roles : [];
|
|
1758
|
+
if (!roles.length) {
|
|
1759
|
+
return res.status(403).json({ error: "Forbidden", reason: "NO_ROLES" });
|
|
1760
|
+
}
|
|
1761
|
+
if (roles.includes("platform_admin")) {
|
|
1762
|
+
return next();
|
|
1763
|
+
}
|
|
1764
|
+
const orgId = user.orgId || user.org_id || user.projectId || null;
|
|
1765
|
+
const rolePermissions = await RolePermissionModel.find({
|
|
1766
|
+
orgId,
|
|
1767
|
+
role: { $in: roles }
|
|
1768
|
+
}).lean().exec();
|
|
1769
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
1770
|
+
for (const rp of rolePermissions) {
|
|
1771
|
+
if (Array.isArray(rp.permissions)) {
|
|
1772
|
+
for (const p of rp.permissions) {
|
|
1773
|
+
allowed.add(p);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (!allowed.has(permissionKey)) {
|
|
1778
|
+
return res.status(403).json({
|
|
1779
|
+
error: "Forbidden",
|
|
1780
|
+
reason: "MISSING_PERMISSION",
|
|
1781
|
+
permission: permissionKey
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
return next();
|
|
1785
|
+
} catch (err) {
|
|
1786
|
+
return next(err);
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/middlewares/requirePermission.ts
|
|
1792
|
+
function requirePermission2(permission) {
|
|
1793
|
+
return (req, res, next) => {
|
|
1794
|
+
const user = req.user;
|
|
1795
|
+
if (!user) {
|
|
1796
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
1797
|
+
}
|
|
1798
|
+
if (!permission) {
|
|
1799
|
+
return next();
|
|
1800
|
+
}
|
|
1801
|
+
if (!hasPermission(user, permission)) {
|
|
1802
|
+
return res.status(403).json({
|
|
1803
|
+
error: "Forbidden",
|
|
1804
|
+
reason: "MISSING_PERMISSION",
|
|
1805
|
+
permission,
|
|
1806
|
+
userPermissions: user.permissions
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
next();
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/services/upload.service.ts
|
|
1814
|
+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
1815
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1816
|
+
var UploadsService = class {
|
|
1817
|
+
s3;
|
|
1818
|
+
constructor() {
|
|
1819
|
+
this.s3 = new S3Client({
|
|
1820
|
+
region: process.env.AWS_REGION,
|
|
1821
|
+
credentials: {
|
|
1822
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
1823
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
async uploadPublicImage(buffer, mimetype, ext) {
|
|
1828
|
+
const key = `${randomUUID4()}.${ext}`;
|
|
1829
|
+
await this.s3.send(
|
|
1830
|
+
new PutObjectCommand({
|
|
1831
|
+
Bucket: process.env.AWS_S3_BUCKET,
|
|
1832
|
+
Key: key,
|
|
1833
|
+
Body: buffer,
|
|
1834
|
+
ACL: "public-read",
|
|
1835
|
+
ContentType: mimetype
|
|
1836
|
+
})
|
|
1837
|
+
);
|
|
1838
|
+
return `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
// src/nest/authx.guard.ts
|
|
1843
|
+
import {
|
|
1844
|
+
ForbiddenException,
|
|
1845
|
+
Injectable,
|
|
1846
|
+
UnauthorizedException
|
|
1847
|
+
} from "@nestjs/common";
|
|
1848
|
+
import { Reflector } from "@nestjs/core";
|
|
1849
|
+
import { AuthGuard } from "@nestjs/passport";
|
|
1850
|
+
|
|
1851
|
+
// src/nest/decorators/permissions.decorator.ts
|
|
1852
|
+
import { SetMetadata } from "@nestjs/common";
|
|
1853
|
+
var PERMISSIONS_KEY = "permissions";
|
|
1854
|
+
var Permissions = (...permissions) => SetMetadata(PERMISSIONS_KEY, permissions);
|
|
1855
|
+
|
|
1856
|
+
// src/nest/decorators/roles.decorator.ts
|
|
1857
|
+
import { SetMetadata as SetMetadata2 } from "@nestjs/common";
|
|
1858
|
+
var ROLES_KEY = "roles";
|
|
1859
|
+
var Roles = (...roles) => SetMetadata2(ROLES_KEY, roles);
|
|
1860
|
+
|
|
1861
|
+
// src/nest/authx.guard.ts
|
|
1862
|
+
var AuthXGuard = class extends AuthGuard("authx") {
|
|
1863
|
+
reflector = new Reflector();
|
|
1864
|
+
constructor() {
|
|
1865
|
+
super();
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Override handleRequest to convert Passport errors to NestJS exceptions
|
|
1869
|
+
* This prevents the "Right-hand side of 'instanceof' is not an object" error
|
|
1870
|
+
*/
|
|
1871
|
+
handleRequest(err, user, info, context) {
|
|
1872
|
+
if (err || !user) {
|
|
1873
|
+
const message = err?.message || info?.message || "Authentication required";
|
|
1874
|
+
throw new UnauthorizedException(message);
|
|
1875
|
+
}
|
|
1876
|
+
return user;
|
|
1877
|
+
}
|
|
1878
|
+
async canActivate(context) {
|
|
1879
|
+
try {
|
|
1880
|
+
let authenticated;
|
|
1881
|
+
try {
|
|
1882
|
+
authenticated = await super.canActivate(context);
|
|
1883
|
+
} catch (passportError) {
|
|
1884
|
+
const message = passportError?.message || "Authentication required";
|
|
1885
|
+
throw new UnauthorizedException(message);
|
|
1886
|
+
}
|
|
1887
|
+
if (!authenticated) {
|
|
1888
|
+
throw new UnauthorizedException("Authentication required");
|
|
1889
|
+
}
|
|
1890
|
+
const request = context.switchToHttp().getRequest();
|
|
1891
|
+
const user = request.user;
|
|
1892
|
+
if (!user) {
|
|
1893
|
+
throw new UnauthorizedException("Authentication required");
|
|
1894
|
+
}
|
|
1895
|
+
const requiredRoles = this.reflector.getAllAndOverride(
|
|
1896
|
+
ROLES_KEY,
|
|
1897
|
+
[context.getHandler(), context.getClass()]
|
|
1898
|
+
);
|
|
1899
|
+
const requiredPermissions = this.reflector.getAllAndOverride(
|
|
1900
|
+
PERMISSIONS_KEY,
|
|
1901
|
+
[context.getHandler(), context.getClass()]
|
|
1902
|
+
);
|
|
1903
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
1904
|
+
if (!hasAnyRole(user, requiredRoles)) {
|
|
1905
|
+
throw new ForbiddenException(
|
|
1906
|
+
`Requires one of roles: ${requiredRoles.join(", ")}`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (requiredPermissions && requiredPermissions.length > 0) {
|
|
1911
|
+
if (!hasAnyPermission(user, requiredPermissions)) {
|
|
1912
|
+
throw new ForbiddenException(
|
|
1913
|
+
`Requires one of permissions: ${requiredPermissions.join(", ")}`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return true;
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
if (error instanceof UnauthorizedException || error instanceof ForbiddenException) {
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1922
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1923
|
+
throw new UnauthorizedException(errorMessage || "Authentication failed");
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
AuthXGuard = __decorateClass([
|
|
1928
|
+
Injectable()
|
|
1929
|
+
], AuthXGuard);
|
|
1930
|
+
|
|
1931
|
+
// src/nest/decorators/session.decorator.ts
|
|
1932
|
+
import { createParamDecorator } from "@nestjs/common";
|
|
1933
|
+
var AuthXSessionDecorator = createParamDecorator(
|
|
1934
|
+
(data, ctx) => {
|
|
1935
|
+
const request = ctx.switchToHttp().getRequest();
|
|
1936
|
+
return request.user || null;
|
|
1937
|
+
}
|
|
1938
|
+
);
|
|
1939
|
+
|
|
1940
|
+
// src/passport/authx.strategy.ts
|
|
1941
|
+
import { Strategy } from "passport";
|
|
1942
|
+
var AuthXStrategy = class extends Strategy {
|
|
1943
|
+
name = "authx";
|
|
1944
|
+
authenticate(req) {
|
|
1945
|
+
try {
|
|
1946
|
+
console.log("AuthXStrategy.authenticate - starting");
|
|
1947
|
+
const token = extractToken(req);
|
|
1948
|
+
console.log("AuthXStrategy.authenticate - token extracted:", token ? "yes" : "no");
|
|
1949
|
+
if (!token) {
|
|
1950
|
+
console.log("AuthXStrategy.authenticate - no token, failing");
|
|
1951
|
+
return this.fail({ message: "Missing token" }, 401);
|
|
1952
|
+
}
|
|
1953
|
+
console.log("AuthXStrategy.authenticate - verifying JWT");
|
|
1954
|
+
verifyJwt(token).then((claims) => {
|
|
1955
|
+
console.log("AuthXStrategy.authenticate - JWT verified successfully");
|
|
1956
|
+
const session = buildSession(claims);
|
|
1957
|
+
req.user = session;
|
|
1958
|
+
return this.success(session);
|
|
1959
|
+
}).catch((error) => {
|
|
1960
|
+
console.log("AuthXStrategy.authenticate - JWT verification failed:", error?.message || error);
|
|
1961
|
+
return this.fail({ message: error?.message || "Unauthorized" }, 401);
|
|
1962
|
+
});
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
console.log("AuthXStrategy.authenticate - exception caught:", error?.message || error);
|
|
1965
|
+
return this.fail({ message: error?.message || "Unauthorized" }, 401);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
function createAuthXStrategy() {
|
|
1970
|
+
return new AuthXStrategy();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/next/server/authx.ts
|
|
1974
|
+
import { cookies } from "next/headers";
|
|
1975
|
+
async function authx() {
|
|
1976
|
+
try {
|
|
1977
|
+
const cookieStore = await cookies();
|
|
1978
|
+
const token = cookieStore.get("access_token")?.value || cookieStore.get("authorization")?.value || cookieStore.get("auth_token")?.value || null;
|
|
1979
|
+
if (!token) {
|
|
1980
|
+
return { session: null };
|
|
1981
|
+
}
|
|
1982
|
+
const claims = await verifyJwt(token);
|
|
1983
|
+
const session = buildSession(claims);
|
|
1984
|
+
return { session };
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
return { session: null };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// src/next/server/withAuthRoute.ts
|
|
1991
|
+
import { NextResponse } from "next/server";
|
|
1992
|
+
function withAuthRoute(permissionOrHandler, handler) {
|
|
1993
|
+
let permission;
|
|
1994
|
+
let routeHandler;
|
|
1995
|
+
if (typeof permissionOrHandler === "string") {
|
|
1996
|
+
permission = permissionOrHandler;
|
|
1997
|
+
routeHandler = handler;
|
|
1998
|
+
} else {
|
|
1999
|
+
permission = void 0;
|
|
2000
|
+
routeHandler = permissionOrHandler;
|
|
2001
|
+
}
|
|
2002
|
+
return async (req, context) => {
|
|
2003
|
+
const { session } = await authx();
|
|
2004
|
+
if (!session) {
|
|
2005
|
+
return new NextResponse(
|
|
2006
|
+
JSON.stringify({ error: "Unauthorized" }),
|
|
2007
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
if (permission && !hasPermission(session, permission)) {
|
|
2011
|
+
return new NextResponse(
|
|
2012
|
+
JSON.stringify({
|
|
2013
|
+
error: "Forbidden",
|
|
2014
|
+
reason: "MISSING_PERMISSION",
|
|
2015
|
+
permission
|
|
2016
|
+
}),
|
|
2017
|
+
{ status: 403, headers: { "Content-Type": "application/json" } }
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
return routeHandler(req, session, context);
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// src/next/client/AuthXProvider.tsx
|
|
2025
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
2026
|
+
import { jsx } from "react/jsx-runtime";
|
|
2027
|
+
var AuthXContext = createContext(void 0);
|
|
2028
|
+
function AuthXProvider({ children, apiUrl = "/auth/me" }) {
|
|
2029
|
+
const [session, setSession] = useState(null);
|
|
2030
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
2031
|
+
const [error, setError] = useState(null);
|
|
2032
|
+
const fetchSession = async () => {
|
|
2033
|
+
try {
|
|
2034
|
+
setIsLoading(true);
|
|
2035
|
+
setError(null);
|
|
2036
|
+
const response = await fetch(apiUrl, {
|
|
2037
|
+
credentials: "include"
|
|
2038
|
+
// Include cookies
|
|
2039
|
+
});
|
|
2040
|
+
if (!response.ok) {
|
|
2041
|
+
if (response.status === 401) {
|
|
2042
|
+
setSession(null);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
throw new Error(`Failed to fetch session: ${response.statusText}`);
|
|
2046
|
+
}
|
|
2047
|
+
const data = await response.json();
|
|
2048
|
+
setSession(data.user || data.session || data);
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
2051
|
+
setSession(null);
|
|
2052
|
+
} finally {
|
|
2053
|
+
setIsLoading(false);
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
useEffect(() => {
|
|
2057
|
+
fetchSession();
|
|
2058
|
+
}, [apiUrl]);
|
|
2059
|
+
const value = {
|
|
2060
|
+
session,
|
|
2061
|
+
isLoading,
|
|
2062
|
+
error,
|
|
2063
|
+
refetch: fetchSession
|
|
2064
|
+
};
|
|
2065
|
+
return /* @__PURE__ */ jsx(AuthXContext.Provider, { value, children });
|
|
2066
|
+
}
|
|
2067
|
+
function useAuthXContext() {
|
|
2068
|
+
const context = useContext(AuthXContext);
|
|
2069
|
+
if (context === void 0) {
|
|
2070
|
+
throw new Error("useAuthX must be used within an AuthXProvider");
|
|
2071
|
+
}
|
|
2072
|
+
return context;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/next/client/HasPermission.tsx
|
|
2076
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
2077
|
+
function HasPermission({
|
|
2078
|
+
permission,
|
|
2079
|
+
children,
|
|
2080
|
+
fallback = null
|
|
2081
|
+
}) {
|
|
2082
|
+
const { session, isLoading } = useAuthXContext();
|
|
2083
|
+
if (isLoading) {
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
if (!session) {
|
|
2087
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
2088
|
+
}
|
|
2089
|
+
const hasPerm = hasPermission(session, permission);
|
|
2090
|
+
if (!hasPerm) {
|
|
2091
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
2092
|
+
}
|
|
2093
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/next/client/HasRole.tsx
|
|
2097
|
+
import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
|
|
2098
|
+
function HasRole({ role, children, fallback = null }) {
|
|
2099
|
+
const { session, isLoading } = useAuthXContext();
|
|
2100
|
+
if (isLoading) {
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
if (!session) {
|
|
2104
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: fallback });
|
|
2105
|
+
}
|
|
2106
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
2107
|
+
const hasRole2 = hasAnyRole(session, roles);
|
|
2108
|
+
if (!hasRole2) {
|
|
2109
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: fallback });
|
|
2110
|
+
}
|
|
2111
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children });
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/next/client/SignedIn.tsx
|
|
2115
|
+
import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
|
|
2116
|
+
function SignedIn({ children, fallback = null }) {
|
|
2117
|
+
const { session, isLoading } = useAuthXContext();
|
|
2118
|
+
if (isLoading) {
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
if (!session) {
|
|
2122
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children: fallback });
|
|
2123
|
+
}
|
|
2124
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children });
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/next/client/SignedOut.tsx
|
|
2128
|
+
import { Fragment as Fragment4, jsx as jsx5 } from "react/jsx-runtime";
|
|
2129
|
+
function SignedOut({ children, fallback = null }) {
|
|
2130
|
+
const { session, isLoading } = useAuthXContext();
|
|
2131
|
+
if (isLoading) {
|
|
2132
|
+
return null;
|
|
2133
|
+
}
|
|
2134
|
+
if (session) {
|
|
2135
|
+
return /* @__PURE__ */ jsx5(Fragment4, { children: fallback });
|
|
2136
|
+
}
|
|
2137
|
+
return /* @__PURE__ */ jsx5(Fragment4, { children });
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/next/client/useAuthX.ts
|
|
2141
|
+
function useAuthX() {
|
|
2142
|
+
const { session, isLoading, error, refetch } = useAuthXContext();
|
|
2143
|
+
return {
|
|
2144
|
+
session,
|
|
2145
|
+
isLoading,
|
|
2146
|
+
error,
|
|
2147
|
+
refetch,
|
|
2148
|
+
isSignedIn: !!session,
|
|
2149
|
+
userId: session?.userId || null,
|
|
2150
|
+
email: session?.email || null,
|
|
2151
|
+
roles: session?.roles || [],
|
|
2152
|
+
permissions: session?.permissions || []
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
function useHasRole(role) {
|
|
2156
|
+
const { session } = useAuthXContext();
|
|
2157
|
+
if (!session || !session.roles) return false;
|
|
2158
|
+
return session.roles.includes(role);
|
|
2159
|
+
}
|
|
2160
|
+
function useHasPermission(permission) {
|
|
2161
|
+
const { session } = useAuthXContext();
|
|
2162
|
+
if (!session || !session.permissions) return false;
|
|
2163
|
+
return session.permissions.includes(permission);
|
|
2164
|
+
}
|
|
2165
|
+
export {
|
|
2166
|
+
AuthAdminService,
|
|
2167
|
+
AuthXGuard,
|
|
2168
|
+
AuthXProvider,
|
|
2169
|
+
AuthXSessionDecorator,
|
|
2170
|
+
AuthXStrategy,
|
|
2171
|
+
EmailService,
|
|
2172
|
+
HasPermission,
|
|
2173
|
+
HasRole,
|
|
2174
|
+
PLATFORM_ROLES,
|
|
2175
|
+
Permissions,
|
|
2176
|
+
ProjectsService,
|
|
2177
|
+
Roles,
|
|
2178
|
+
SignedIn,
|
|
2179
|
+
SignedOut,
|
|
2180
|
+
UploadsService,
|
|
2181
|
+
authorize,
|
|
2182
|
+
authx,
|
|
2183
|
+
buildSession,
|
|
2184
|
+
createAuthXStrategy,
|
|
2185
|
+
express_exports as express,
|
|
2186
|
+
getPermissionsForRoles,
|
|
2187
|
+
hasAllPermissions,
|
|
2188
|
+
hasAllRoles,
|
|
2189
|
+
hasAnyPermission,
|
|
2190
|
+
hasAnyRole,
|
|
2191
|
+
hasPermission,
|
|
2192
|
+
hasRole,
|
|
2193
|
+
nest,
|
|
2194
|
+
requireAuth,
|
|
2195
|
+
requirePermission2 as requirePermission,
|
|
2196
|
+
requirePermission as requirePermissionLegacy,
|
|
2197
|
+
requireRole,
|
|
2198
|
+
useAuthX,
|
|
2199
|
+
useAuthXContext,
|
|
2200
|
+
useHasPermission,
|
|
2201
|
+
useHasRole,
|
|
2202
|
+
withAuthRoute
|
|
2203
|
+
};
|
|
2204
|
+
//# sourceMappingURL=index.js.map
|