@axiom-lattice/gateway 2.1.28 → 2.1.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.28",
3
+ "version": "2.1.30",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -36,9 +36,9 @@
36
36
  "pino-roll": "^3.1.0",
37
37
  "redis": "^5.0.1",
38
38
  "uuid": "^9.0.1",
39
- "@axiom-lattice/core": "2.1.23",
40
- "@axiom-lattice/protocols": "2.1.13",
41
- "@axiom-lattice/queue-redis": "1.0.12"
39
+ "@axiom-lattice/core": "2.1.25",
40
+ "@axiom-lattice/protocols": "2.1.15",
41
+ "@axiom-lattice/queue-redis": "1.0.14"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/jest": "^29.5.14",
@@ -0,0 +1,443 @@
1
+ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
+ import { getStoreLattice } from "@axiom-lattice/core";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import type { CreateUserRequest, CreateTenantRequest, UserTenantRole } from "@axiom-lattice/protocols";
5
+
6
+ interface RegisterRequest {
7
+ email: string;
8
+ password: string;
9
+ name: string;
10
+ }
11
+
12
+ interface LoginRequest {
13
+ email: string;
14
+ password: string;
15
+ }
16
+
17
+ interface SelectTenantRequest {
18
+ tenantId: string;
19
+ }
20
+
21
+ interface ApproveUserRequest {
22
+ userId: string;
23
+ approved: boolean;
24
+ }
25
+
26
+ interface AssignTenantRequest {
27
+ userId: string;
28
+ tenantId: string;
29
+ role?: UserTenantRole;
30
+ }
31
+
32
+ export interface AuthConfig {
33
+ autoApproveUsers: boolean;
34
+ allowTenantRegistration: boolean;
35
+ jwtSecret?: string;
36
+ tokenExpiration: number;
37
+ }
38
+
39
+ const defaultAuthConfig: AuthConfig = {
40
+ autoApproveUsers: true,
41
+ allowTenantRegistration: true,
42
+ tokenExpiration: 86400,
43
+ };
44
+
45
+ export class AuthController {
46
+ private userStore;
47
+ private tenantStore;
48
+ private userTenantLinkStore;
49
+ private config: AuthConfig;
50
+
51
+ constructor(config: Partial<AuthConfig> = {}) {
52
+ this.userStore = getStoreLattice("default", "user").store;
53
+ this.tenantStore = getStoreLattice("default", "tenant").store;
54
+ this.userTenantLinkStore = getStoreLattice("default", "userTenantLink").store;
55
+ this.config = { ...defaultAuthConfig, ...config };
56
+ }
57
+
58
+ async register(
59
+ request: FastifyRequest<{ Body: RegisterRequest }>,
60
+ reply: FastifyReply
61
+ ) {
62
+ const { email, password, name } = request.body;
63
+
64
+ try {
65
+ const existingUser = await this.userStore.getUserByEmail(email);
66
+ if (existingUser) {
67
+ return reply.status(409).send({
68
+ success: false,
69
+ error: "User with this email already exists",
70
+ });
71
+ }
72
+
73
+ const userId = uuidv4();
74
+ const userStatus = this.config.autoApproveUsers ? "active" : "pending";
75
+
76
+ const userData: CreateUserRequest = {
77
+ email,
78
+ name,
79
+ status: userStatus,
80
+ metadata: {
81
+ passwordHash: await this.hashPassword(password),
82
+ },
83
+ };
84
+
85
+ const user = await this.userStore.createUser(userId, userData);
86
+
87
+ return reply.status(201).send({
88
+ success: true,
89
+ message: this.config.autoApproveUsers
90
+ ? "Registration successful"
91
+ : "Registration successful. Please wait for admin approval.",
92
+ data: {
93
+ user: {
94
+ id: user.id,
95
+ email: user.email,
96
+ name: user.name,
97
+ status: user.status,
98
+ },
99
+ },
100
+ });
101
+ } catch (error) {
102
+ console.error("Registration error:", error);
103
+ return reply.status(500).send({
104
+ success: false,
105
+ error: "Registration failed",
106
+ });
107
+ }
108
+ }
109
+
110
+ async login(
111
+ request: FastifyRequest<{ Body: LoginRequest }>,
112
+ reply: FastifyReply
113
+ ) {
114
+ const { email, password } = request.body;
115
+
116
+ try {
117
+ const user = await this.userStore.getUserByEmail(email);
118
+ if (!user) {
119
+ return reply.status(401).send({
120
+ success: false,
121
+ error: "Invalid credentials",
122
+ });
123
+ }
124
+
125
+ if (user.status !== "active") {
126
+ return reply.status(403).send({
127
+ success: false,
128
+ error: `Account is ${user.status}. Please wait for admin approval.`,
129
+ });
130
+ }
131
+
132
+ const isValidPassword = await this.verifyPassword(
133
+ password,
134
+ user.metadata?.passwordHash || ""
135
+ );
136
+
137
+ if (!isValidPassword) {
138
+ return reply.status(401).send({
139
+ success: false,
140
+ error: "Invalid credentials",
141
+ });
142
+ }
143
+
144
+ const tenantLinks = await this.userTenantLinkStore.getTenantsByUser(user.id);
145
+
146
+ const token = await this.generateToken(user.id);
147
+
148
+ return {
149
+ success: true,
150
+ data: {
151
+ user: {
152
+ id: user.id,
153
+ email: user.email,
154
+ name: user.name,
155
+ status: user.status,
156
+ },
157
+ tenants: tenantLinks.map(link => ({
158
+ id: link.tenantId,
159
+ role: link.role,
160
+ })),
161
+ token,
162
+ requiresTenantSelection: tenantLinks.length > 1,
163
+ hasTenants: tenantLinks.length > 0,
164
+ },
165
+ };
166
+ } catch (error) {
167
+ console.error("Login error:", error);
168
+ return reply.status(500).send({
169
+ success: false,
170
+ error: "Login failed",
171
+ });
172
+ }
173
+ }
174
+
175
+ async getUserTenants(
176
+ request: FastifyRequest,
177
+ reply: FastifyReply
178
+ ) {
179
+ const userId = (request as any).user?.id;
180
+
181
+ if (!userId) {
182
+ return reply.status(401).send({
183
+ success: false,
184
+ error: "Unauthorized",
185
+ });
186
+ }
187
+
188
+ try {
189
+ const links = await this.userTenantLinkStore.getTenantsByUser(userId);
190
+
191
+ const tenantsWithDetails = await Promise.all(
192
+ links.map(async (link) => {
193
+ const tenant = await this.tenantStore.getTenantById(link.tenantId);
194
+ return {
195
+ ...link,
196
+ tenant: tenant || null,
197
+ };
198
+ })
199
+ );
200
+
201
+ return {
202
+ success: true,
203
+ data: tenantsWithDetails,
204
+ };
205
+ } catch (error) {
206
+ console.error("Get user tenants error:", error);
207
+ return reply.status(500).send({
208
+ success: false,
209
+ error: "Failed to get user tenants",
210
+ });
211
+ }
212
+ }
213
+
214
+ async selectTenant(
215
+ request: FastifyRequest<{ Body: SelectTenantRequest }>,
216
+ reply: FastifyReply
217
+ ) {
218
+ const userId = (request as any).user?.id;
219
+ const { tenantId } = request.body;
220
+
221
+ if (!userId) {
222
+ return reply.status(401).send({
223
+ success: false,
224
+ error: "Unauthorized",
225
+ });
226
+ }
227
+
228
+ try {
229
+ const hasLink = await this.userTenantLinkStore.hasLink(userId, tenantId);
230
+
231
+ if (!hasLink) {
232
+ return reply.status(403).send({
233
+ success: false,
234
+ error: "You do not have access to this tenant",
235
+ });
236
+ }
237
+
238
+ const tenant = await this.tenantStore.getTenantById(tenantId);
239
+
240
+ if (!tenant) {
241
+ return reply.status(404).send({
242
+ success: false,
243
+ error: "Tenant not found",
244
+ });
245
+ }
246
+
247
+ const token = await this.generateToken(userId, tenantId);
248
+
249
+ return {
250
+ success: true,
251
+ data: {
252
+ tenant: {
253
+ id: tenant.id,
254
+ name: tenant.name,
255
+ status: tenant.status,
256
+ },
257
+ token,
258
+ },
259
+ };
260
+ } catch (error) {
261
+ console.error("Select tenant error:", error);
262
+ return reply.status(500).send({
263
+ success: false,
264
+ error: "Failed to select tenant",
265
+ });
266
+ }
267
+ }
268
+
269
+ async approveUser(
270
+ request: FastifyRequest<{ Body: ApproveUserRequest }>,
271
+ reply: FastifyReply
272
+ ) {
273
+ const { userId, approved } = request.body;
274
+
275
+ try {
276
+ const user = await this.userStore.getUserById(userId);
277
+ if (!user) {
278
+ return reply.status(404).send({
279
+ success: false,
280
+ error: "User not found",
281
+ });
282
+ }
283
+
284
+ if (user.status !== "pending") {
285
+ return reply.status(400).send({
286
+ success: false,
287
+ error: `User is already ${user.status}`,
288
+ });
289
+ }
290
+
291
+ const updatedUser = await this.userStore.updateUser(userId, {
292
+ status: approved ? "active" : "suspended",
293
+ });
294
+
295
+ return {
296
+ success: true,
297
+ data: updatedUser,
298
+ };
299
+ } catch (error) {
300
+ console.error("Approval error:", error);
301
+ return reply.status(500).send({
302
+ success: false,
303
+ error: "Approval failed",
304
+ });
305
+ }
306
+ }
307
+
308
+ async listPendingUsers(
309
+ request: FastifyRequest,
310
+ reply: FastifyReply
311
+ ) {
312
+ try {
313
+ const users = await this.userStore.getAllUsers();
314
+ const pendingUsers = users.filter((u) => u.status === "pending");
315
+
316
+ return {
317
+ success: true,
318
+ data: pendingUsers,
319
+ };
320
+ } catch (error) {
321
+ console.error("List pending users error:", error);
322
+ return reply.status(500).send({
323
+ success: false,
324
+ error: "Failed to list pending users",
325
+ });
326
+ }
327
+ }
328
+
329
+ async assignTenant(
330
+ request: FastifyRequest<{ Body: AssignTenantRequest }>,
331
+ reply: FastifyReply
332
+ ) {
333
+ const { userId, tenantId, role = "member" } = request.body;
334
+
335
+ try {
336
+ const user = await this.userStore.getUserById(userId);
337
+ if (!user) {
338
+ return reply.status(404).send({
339
+ success: false,
340
+ error: "User not found",
341
+ });
342
+ }
343
+
344
+ const tenant = await this.tenantStore.getTenantById(tenantId);
345
+ if (!tenant) {
346
+ return reply.status(404).send({
347
+ success: false,
348
+ error: "Tenant not found",
349
+ });
350
+ }
351
+
352
+ const link = await this.userTenantLinkStore.createLink({
353
+ userId,
354
+ tenantId,
355
+ role,
356
+ });
357
+
358
+ return {
359
+ success: true,
360
+ data: link,
361
+ };
362
+ } catch (error) {
363
+ console.error("Assign tenant error:", error);
364
+ return reply.status(500).send({
365
+ success: false,
366
+ error: "Failed to assign tenant",
367
+ });
368
+ }
369
+ }
370
+
371
+ private async hashPassword(password: string): Promise<string> {
372
+ const encoder = new TextEncoder();
373
+ const data = encoder.encode(password + "salt");
374
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
375
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
376
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
377
+ }
378
+
379
+ private async verifyPassword(password: string, hash: string): Promise<boolean> {
380
+ const hashedPassword = await this.hashPassword(password);
381
+ return hashedPassword === hash;
382
+ }
383
+
384
+ private async generateToken(userId: string, tenantId?: string): Promise<string> {
385
+ const payload: any = {
386
+ userId,
387
+ exp: Date.now() + this.config.tokenExpiration * 1000,
388
+ };
389
+ if (tenantId) {
390
+ payload.tenantId = tenantId;
391
+ }
392
+ return btoa(JSON.stringify(payload));
393
+ }
394
+ }
395
+
396
+ export function registerAuthRoutes(
397
+ app: FastifyInstance,
398
+ config?: Partial<AuthConfig>
399
+ ) {
400
+ const controller = new AuthController(config);
401
+
402
+ const authHook = async (request: FastifyRequest, reply: FastifyReply) => {
403
+ const authHeader = request.headers.authorization;
404
+
405
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
406
+ return reply.status(401).send({
407
+ success: false,
408
+ error: "Unauthorized - Missing or invalid token",
409
+ });
410
+ }
411
+
412
+ const token = authHeader.substring(7);
413
+
414
+ try {
415
+ const payload = JSON.parse(atob(token));
416
+
417
+ if (payload.exp && payload.exp < Date.now()) {
418
+ return reply.status(401).send({
419
+ success: false,
420
+ error: "Unauthorized - Token expired",
421
+ });
422
+ }
423
+
424
+ (request as any).user = {
425
+ id: payload.userId,
426
+ tenantId: payload.tenantId,
427
+ };
428
+ } catch {
429
+ return reply.status(401).send({
430
+ success: false,
431
+ error: "Unauthorized - Invalid token",
432
+ });
433
+ }
434
+ };
435
+
436
+ app.post("/api/auth/register", controller.register.bind(controller));
437
+ app.post("/api/auth/login", controller.login.bind(controller));
438
+ app.get("/api/auth/tenants", { preHandler: authHook }, (req, res) => controller.getUserTenants(req, res));
439
+ app.post("/api/auth/select-tenant", { preHandler: authHook }, (req, res) => controller.selectTenant(req as any, res));
440
+ app.post("/api/auth/approve", { preHandler: authHook }, (req, res) => controller.approveUser(req as any, res));
441
+ app.get("/api/auth/pending", { preHandler: authHook }, (req, res) => controller.listPendingUsers(req, res));
442
+ app.post("/api/auth/assign-tenant", { preHandler: authHook }, (req, res) => controller.assignTenant(req as any, res));
443
+ }