@xcelsior/auth 1.0.0 → 1.1.0

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/dist/index.js CHANGED
@@ -32,7 +32,9 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AuthMiddleware: () => AuthMiddleware,
34
34
  AuthService: () => AuthService,
35
- UserRoleSchema: () => UserRoleSchema,
35
+ CreateSessionSchema: () => CreateSessionSchema,
36
+ CreateUserSchema: () => CreateUserSchema,
37
+ SessionSchema: () => SessionSchema,
36
38
  UserSchema: () => UserSchema
37
39
  });
38
40
  module.exports = __toCommonJS(index_exports);
@@ -41,170 +43,10 @@ module.exports = __toCommonJS(index_exports);
41
43
  var import_bcryptjs = __toESM(require("bcryptjs"));
42
44
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
43
45
  var import_uuid = require("uuid");
44
-
45
- // src/storage/dynamodb.ts
46
- var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
47
- var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
48
- var DynamoDBStorageProvider = class {
49
- constructor(config) {
50
- const dbClient = new import_client_dynamodb.DynamoDBClient({ region: config.region });
51
- this.client = import_lib_dynamodb.DynamoDBDocumentClient.from(dbClient, {});
52
- this.tableName = config.tableName;
53
- }
54
- async createUser(user) {
55
- await this.client.send(
56
- new import_lib_dynamodb.PutCommand({
57
- TableName: this.tableName,
58
- Item: user,
59
- ConditionExpression: "attribute_not_exists(email)"
60
- })
61
- );
62
- }
63
- async getUserByResetPasswordToken(resetPasswordToken) {
64
- const response = await this.client.send(
65
- new import_lib_dynamodb.QueryCommand({
66
- TableName: this.tableName,
67
- IndexName: "ResetPasswordTokenIndex",
68
- KeyConditionExpression: "resetPasswordToken = :token",
69
- ExpressionAttributeValues: {
70
- ":token": resetPasswordToken
71
- }
72
- })
73
- );
74
- return response.Items?.[0];
75
- }
76
- async getUserByVerifyEmailToken(verifyEmailToken) {
77
- const response = await this.client.send(
78
- new import_lib_dynamodb.QueryCommand({
79
- TableName: this.tableName,
80
- IndexName: "VerifyEmailTokenIndex",
81
- KeyConditionExpression: "verificationToken = :token",
82
- ExpressionAttributeValues: {
83
- ":token": verifyEmailToken
84
- }
85
- })
86
- );
87
- return response.Items?.[0];
88
- }
89
- async getUserById(id) {
90
- const response = await this.client.send(
91
- new import_lib_dynamodb.GetCommand({
92
- TableName: this.tableName,
93
- Key: { id }
94
- })
95
- );
96
- return response.Item;
97
- }
98
- async getUserByEmail(email) {
99
- const response = await this.client.send(
100
- new import_lib_dynamodb.QueryCommand({
101
- TableName: this.tableName,
102
- IndexName: "EmailIndex",
103
- KeyConditionExpression: "email = :email",
104
- ExpressionAttributeValues: {
105
- ":email": email
106
- }
107
- })
108
- );
109
- return response.Items?.[0];
110
- }
111
- async updateUser(id, updates) {
112
- const toSet = {};
113
- const toRemove = [];
114
- Object.entries(updates).forEach(([key, value]) => {
115
- if (value === void 0) {
116
- toRemove.push(key);
117
- } else {
118
- toSet[key] = value;
119
- }
120
- });
121
- if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
122
- return;
123
- }
124
- const setPart = Object.keys(toSet).length > 0 ? `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}` : "";
125
- const removePart = toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(", ")}` : "";
126
- const updateExpression = [setPart, removePart].filter(Boolean).join(" ");
127
- const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
128
- (acc, key) => ({ ...acc, [`#${key}`]: key }),
129
- {}
130
- );
131
- const expressionAttributeValues = Object.entries(toSet).reduce(
132
- (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
133
- {}
134
- );
135
- await this.client.send(
136
- new import_lib_dynamodb.UpdateCommand({
137
- TableName: this.tableName,
138
- Key: { id },
139
- UpdateExpression: updateExpression,
140
- ExpressionAttributeNames: expressionAttributeNames,
141
- ...Object.keys(expressionAttributeValues).length > 0 && {
142
- ExpressionAttributeValues: expressionAttributeValues
143
- }
144
- })
145
- );
146
- }
147
- async deleteUser(id) {
148
- await this.client.send(
149
- new import_lib_dynamodb.DeleteCommand({
150
- TableName: this.tableName,
151
- Key: { id }
152
- })
153
- );
154
- }
155
- };
156
-
157
- // src/storage/index.ts
158
- function createStorageProvider(config) {
159
- switch (config.type) {
160
- case "dynamodb":
161
- return new DynamoDBStorageProvider(config.options);
162
- case "mongodb":
163
- throw new Error("MongoDB storage provider not implemented yet");
164
- case "postgres":
165
- throw new Error("PostgreSQL storage provider not implemented yet");
166
- default:
167
- throw new Error(`Unsupported storage type: ${config.type}`);
168
- }
169
- }
46
+ var import_crypto = __toESM(require("crypto"));
170
47
 
171
48
  // src/email/smtp.ts
172
49
  var import_nodemailer = __toESM(require("nodemailer"));
173
- var SMTPEmailProvider = class {
174
- constructor(from, config, templates) {
175
- this.transporter = import_nodemailer.default.createTransport(config);
176
- this.config = { from, templates };
177
- }
178
- async sendVerificationEmail(email, token) {
179
- const { subject, html } = this.config.templates.verification;
180
- await this.transporter.sendMail({
181
- from: this.config.from,
182
- to: email,
183
- subject,
184
- html: html(token)
185
- });
186
- }
187
- async sendPasswordResetEmail(email, token) {
188
- const { subject, html } = this.config.templates.resetPassword;
189
- await this.transporter.sendMail({
190
- from: this.config.from,
191
- to: email,
192
- subject,
193
- html: html(token)
194
- });
195
- }
196
- async verifyConnection() {
197
- try {
198
- await this.transporter.verify();
199
- return true;
200
- } catch (_error) {
201
- return false;
202
- }
203
- }
204
- };
205
-
206
- // src/email/ses.ts
207
- var import_client_sesv2 = require("@aws-sdk/client-sesv2");
208
50
 
209
51
  // src/email/defaultTemplates.ts
210
52
  var defaultTemplates = {
@@ -263,7 +105,42 @@ var defaultTemplates = {
263
105
  }
264
106
  };
265
107
 
108
+ // src/email/smtp.ts
109
+ var SMTPEmailProvider = class {
110
+ constructor(from, config, templates) {
111
+ this.transporter = import_nodemailer.default.createTransport(config);
112
+ this.config = { from, templates: templates ?? defaultTemplates };
113
+ }
114
+ async sendVerificationEmail(email, token) {
115
+ const { subject, html } = this.config.templates.verification;
116
+ await this.transporter.sendMail({
117
+ from: this.config.from,
118
+ to: email,
119
+ subject,
120
+ html: html(token)
121
+ });
122
+ }
123
+ async sendPasswordResetEmail(email, token) {
124
+ const { subject, html } = this.config.templates.resetPassword;
125
+ await this.transporter.sendMail({
126
+ from: this.config.from,
127
+ to: email,
128
+ subject,
129
+ html: html(token)
130
+ });
131
+ }
132
+ async verifyConnection() {
133
+ try {
134
+ await this.transporter.verify();
135
+ return true;
136
+ } catch (_error) {
137
+ return false;
138
+ }
139
+ }
140
+ };
141
+
266
142
  // src/email/ses.ts
143
+ var import_client_sesv2 = require("@aws-sdk/client-sesv2");
267
144
  var SESEmailProvider = class {
268
145
  constructor(from, config, templates) {
269
146
  this.client = new import_client_sesv2.SESv2Client({
@@ -353,10 +230,10 @@ function createEmailProvider(config) {
353
230
  var AuthService = class {
354
231
  constructor(config) {
355
232
  this.config = config;
356
- this.storage = createStorageProvider(config.storage);
233
+ this.storage = config.storage;
357
234
  this.email = createEmailProvider(config.email);
358
235
  }
359
- generateToken(user) {
236
+ generateAccessToken(user) {
360
237
  return import_jsonwebtoken.default.sign(
361
238
  {
362
239
  id: user.id,
@@ -372,31 +249,100 @@ var AuthService = class {
372
249
  }
373
250
  );
374
251
  }
252
+ generateRefreshToken(sessionId, userId) {
253
+ const payload = {
254
+ sessionId,
255
+ userId,
256
+ type: "refresh"
257
+ };
258
+ return import_jsonwebtoken.default.sign(payload, this.config.jwt.privateKey, {
259
+ algorithm: "RS256",
260
+ expiresIn: this.config.jwt.refreshTokenExpiresIn || "7d",
261
+ keyid: this.config.jwt.keyId
262
+ });
263
+ }
264
+ verifyRefreshToken(token) {
265
+ try {
266
+ const payload = import_jsonwebtoken.default.verify(token, this.config.jwt.publicKey, {
267
+ algorithms: ["RS256"]
268
+ });
269
+ if (payload.type !== "refresh") {
270
+ throw new Error("Invalid token type");
271
+ }
272
+ return payload;
273
+ } catch (_error) {
274
+ throw new Error("Invalid refresh token");
275
+ }
276
+ }
277
+ hashToken(token) {
278
+ return import_crypto.default.createHash("sha256").update(token).digest("hex");
279
+ }
280
+ getRefreshTokenExpiryMs() {
281
+ const expiresIn = this.config.jwt.refreshTokenExpiresIn || "7d";
282
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
283
+ if (!match) {
284
+ return 7 * 24 * 60 * 60 * 1e3;
285
+ }
286
+ const value = parseInt(match[1], 10);
287
+ const unit = match[2];
288
+ const multipliers = {
289
+ s: 1e3,
290
+ m: 60 * 1e3,
291
+ h: 60 * 60 * 1e3,
292
+ d: 24 * 60 * 60 * 1e3
293
+ };
294
+ return value * multipliers[unit];
295
+ }
375
296
  async hashPassword(password) {
376
297
  return import_bcryptjs.default.hash(password, 10);
377
298
  }
378
- async signup(email, password, roles = ["USER"]) {
379
- const existingUser = await this.storage.getUserByEmail(email);
299
+ async createSession(userId, options = {}) {
300
+ const now = Date.now();
301
+ const sessionId = this.config.idGeneration === "uuid" ? (0, import_uuid.v4)() : void 0;
302
+ const tempSession = {
303
+ ...sessionId ? { id: sessionId } : {},
304
+ userId,
305
+ refreshTokenHash: "",
306
+ userAgent: options.userAgent,
307
+ ipAddress: options.ipAddress,
308
+ deviceName: options.deviceName,
309
+ createdAt: now,
310
+ lastUsedAt: now,
311
+ expiresAt: now + this.getRefreshTokenExpiryMs()
312
+ };
313
+ const session = await this.storage.createSession(tempSession);
314
+ const refreshToken = this.generateRefreshToken(session.id, userId);
315
+ await this.storage.updateSession(session.id, {
316
+ refreshTokenHash: this.hashToken(refreshToken)
317
+ });
318
+ return {
319
+ session: { ...session, refreshTokenHash: this.hashToken(refreshToken) },
320
+ refreshToken
321
+ };
322
+ }
323
+ async signup(createUserInput, password, options = {}) {
324
+ const existingUser = await this.storage.getUserByEmail(createUserInput.email);
380
325
  if (existingUser) {
381
326
  throw new Error("User already exists");
382
327
  }
383
328
  const verificationToken = (0, import_uuid.v4)();
329
+ const id = this.config.idGeneration === "uuid" ? (0, import_uuid.v4)() : void 0;
384
330
  const user = {
385
- id: (0, import_uuid.v4)(),
386
- email,
331
+ ...createUserInput,
332
+ id,
387
333
  passwordHash: await this.hashPassword(password),
388
- roles,
389
334
  isEmailVerified: false,
390
335
  verificationToken,
391
336
  createdAt: Date.now(),
392
337
  updatedAt: Date.now()
393
338
  };
394
- await this.storage.createUser(user);
395
- await this.email.sendVerificationEmail(email, verificationToken);
396
- const token = this.generateToken(user);
397
- return { user, token };
339
+ const createdUser = await this.storage.createUser(user);
340
+ await this.email.sendVerificationEmail(createUserInput.email, verificationToken);
341
+ const { refreshToken } = await this.createSession(createdUser.id, options);
342
+ const accessToken = this.generateAccessToken(createdUser);
343
+ return { user: createdUser, tokens: { accessToken, refreshToken } };
398
344
  }
399
- async signin(email, password) {
345
+ async signin(email, password, options = {}) {
400
346
  const user = await this.storage.getUserByEmail(email);
401
347
  if (!user) {
402
348
  throw new Error("Invalid credentials");
@@ -408,8 +354,86 @@ var AuthService = class {
408
354
  if (!user.isEmailVerified) {
409
355
  throw new Error("Please verify your email before signing in");
410
356
  }
411
- const token = this.generateToken(user);
412
- return { user, token };
357
+ const { refreshToken } = await this.createSession(user.id, options);
358
+ const accessToken = this.generateAccessToken(user);
359
+ return { user, tokens: { accessToken, refreshToken } };
360
+ }
361
+ async refreshAccessToken(refreshToken) {
362
+ const payload = this.verifyRefreshToken(refreshToken);
363
+ const session = await this.storage.getSessionById(payload.sessionId);
364
+ if (!session) {
365
+ throw new Error("Session not found");
366
+ }
367
+ const tokenHash = this.hashToken(refreshToken);
368
+ if (session.refreshTokenHash !== tokenHash) {
369
+ throw new Error("Invalid refresh token");
370
+ }
371
+ if (session.expiresAt < Date.now()) {
372
+ await this.storage.deleteSession(session.id);
373
+ throw new Error("Session expired");
374
+ }
375
+ const user = await this.storage.getUserById(session.userId);
376
+ if (!user) {
377
+ throw new Error("User not found");
378
+ }
379
+ const newRefreshToken = this.generateRefreshToken(session.id, user.id);
380
+ await this.storage.updateSession(session.id, {
381
+ refreshTokenHash: this.hashToken(newRefreshToken),
382
+ lastUsedAt: Date.now(),
383
+ expiresAt: Date.now() + this.getRefreshTokenExpiryMs()
384
+ });
385
+ const accessToken = this.generateAccessToken(user);
386
+ return { accessToken, refreshToken: newRefreshToken };
387
+ }
388
+ /**
389
+ * Logout from current session only
390
+ */
391
+ async logout(refreshToken) {
392
+ try {
393
+ const payload = this.verifyRefreshToken(refreshToken);
394
+ await this.storage.deleteSession(payload.sessionId);
395
+ } catch {
396
+ }
397
+ }
398
+ /**
399
+ * Revoke a specific session by ID
400
+ */
401
+ async revokeSession(userId, sessionId) {
402
+ const session = await this.storage.getSessionById(sessionId);
403
+ if (!session || session.userId !== userId) {
404
+ throw new Error("Session not found");
405
+ }
406
+ await this.storage.deleteSession(sessionId);
407
+ }
408
+ /**
409
+ * Revoke all sessions for a user (logout from all devices)
410
+ */
411
+ async revokeAllSessions(userId) {
412
+ await this.storage.deleteAllUserSessions(userId);
413
+ }
414
+ /**
415
+ * Get all active sessions for a user
416
+ */
417
+ async getSessions(userId, currentRefreshToken) {
418
+ const sessions = await this.storage.getSessionsByUserId(userId);
419
+ const now = Date.now();
420
+ let currentSessionId;
421
+ if (currentRefreshToken) {
422
+ try {
423
+ const payload = this.verifyRefreshToken(currentRefreshToken);
424
+ currentSessionId = payload.sessionId;
425
+ } catch {
426
+ }
427
+ }
428
+ return sessions.filter((s) => s.expiresAt > now).map((session) => ({
429
+ id: session.id,
430
+ deviceName: session.deviceName,
431
+ userAgent: session.userAgent,
432
+ ipAddress: session.ipAddress,
433
+ createdAt: session.createdAt,
434
+ lastUsedAt: session.lastUsedAt,
435
+ isCurrent: session.id === currentSessionId
436
+ }));
413
437
  }
414
438
  async verifyEmail(token) {
415
439
  const user = await this.storage.getUserByVerifyEmailToken(token);
@@ -452,6 +476,7 @@ var AuthService = class {
452
476
  try {
453
477
  return import_jsonwebtoken.default.verify(token, this.config.jwt.publicKey, { algorithms: ["RS256"] });
454
478
  } catch (_error) {
479
+ console.log("Token verification failed:", _error);
455
480
  throw new Error("Invalid token");
456
481
  }
457
482
  }
@@ -462,6 +487,74 @@ var AuthService = class {
462
487
  }
463
488
  return requiredRoles.some((role) => user.roles.includes(role));
464
489
  }
490
+ // ==================== User Management ====================
491
+ /**
492
+ * Get a user by ID
493
+ */
494
+ async getUserById(id) {
495
+ return this.storage.getUserById(id);
496
+ }
497
+ /**
498
+ * Get a user by email
499
+ */
500
+ async getUserByEmail(email) {
501
+ return this.storage.getUserByEmail(email);
502
+ }
503
+ /**
504
+ * Update a user's profile information.
505
+ * Supports predefined fields (email, firstName, lastName, roles, isEmailVerified)
506
+ * as well as any additional custom fields.
507
+ */
508
+ async updateUser(id, updates) {
509
+ const user = await this.storage.getUserById(id);
510
+ if (!user) {
511
+ throw new Error("User not found");
512
+ }
513
+ if (updates.email !== void 0 && updates.email !== user.email) {
514
+ const existingUser = await this.storage.getUserByEmail(updates.email);
515
+ if (existingUser) {
516
+ throw new Error("Email already in use");
517
+ }
518
+ }
519
+ const { email, firstName, lastName, roles, isEmailVerified, ...additionalFields } = updates;
520
+ const mergedUpdates = {
521
+ updatedAt: Date.now(),
522
+ ...additionalFields
523
+ };
524
+ if (email !== void 0) mergedUpdates.email = email;
525
+ if (firstName !== void 0) mergedUpdates.firstName = firstName;
526
+ if (lastName !== void 0) mergedUpdates.lastName = lastName;
527
+ if (roles !== void 0) mergedUpdates.roles = roles;
528
+ if (isEmailVerified !== void 0) mergedUpdates.isEmailVerified = isEmailVerified;
529
+ await this.storage.updateUser(id, mergedUpdates);
530
+ return { ...user, ...mergedUpdates };
531
+ }
532
+ /**
533
+ * Delete a user and all their sessions
534
+ */
535
+ async deleteUser(id) {
536
+ const user = await this.storage.getUserById(id);
537
+ if (!user) {
538
+ throw new Error("User not found");
539
+ }
540
+ await this.storage.deleteUser(id);
541
+ }
542
+ /**
543
+ * Find users with filtering and pagination
544
+ *
545
+ * @example
546
+ * // Find all admins
547
+ * const { users } = await authService.findUsers({ hasAnyRole: ['ADMIN'] });
548
+ *
549
+ * // Find verified users with email containing '@company.com'
550
+ * const result = await authService.findUsers({
551
+ * emailContains: '@company.com',
552
+ * isEmailVerified: true
553
+ * }, { limit: 20 });
554
+ */
555
+ async findUsers(filter, options) {
556
+ return this.storage.findUsers(filter, options);
557
+ }
465
558
  };
466
559
 
467
560
  // src/middleware/auth.ts
@@ -476,8 +569,7 @@ var AuthMiddleware = class {
476
569
  if (!token) {
477
570
  throw new Error("No token provided");
478
571
  }
479
- const decoded = await this.authService.verifyToken(token);
480
- req.user = decoded;
572
+ req.user = await this.authService.verifyToken(token);
481
573
  next();
482
574
  } catch (_error) {
483
575
  res.status(401).json({ error: "Unauthorized" });
@@ -509,12 +601,13 @@ var AuthMiddleware = class {
509
601
 
510
602
  // src/types/index.ts
511
603
  var import_zod = require("zod");
512
- var UserRoleSchema = import_zod.z.enum(["ADMIN", "USER", "GUEST"]);
513
604
  var UserSchema = import_zod.z.object({
514
- id: import_zod.z.string(),
605
+ id: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]),
515
606
  email: import_zod.z.string().email(),
607
+ firstName: import_zod.z.string().optional(),
608
+ lastName: import_zod.z.string().optional(),
516
609
  passwordHash: import_zod.z.string(),
517
- roles: import_zod.z.array(UserRoleSchema),
610
+ roles: import_zod.z.array(import_zod.z.string()),
518
611
  isEmailVerified: import_zod.z.boolean(),
519
612
  verificationToken: import_zod.z.string().optional(),
520
613
  resetPasswordToken: import_zod.z.string().optional(),
@@ -522,11 +615,42 @@ var UserSchema = import_zod.z.object({
522
615
  createdAt: import_zod.z.number(),
523
616
  updatedAt: import_zod.z.number()
524
617
  });
618
+ var CreateUserSchema = UserSchema.omit({
619
+ id: true,
620
+ createdAt: true,
621
+ updatedAt: true
622
+ }).extend({
623
+ createdAt: import_zod.z.number().optional(),
624
+ updatedAt: import_zod.z.number().optional(),
625
+ id: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional()
626
+ });
627
+ var SessionSchema = import_zod.z.object({
628
+ id: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]),
629
+ userId: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]),
630
+ refreshTokenHash: import_zod.z.string(),
631
+ userAgent: import_zod.z.string().optional(),
632
+ ipAddress: import_zod.z.string().optional(),
633
+ deviceName: import_zod.z.string().optional(),
634
+ createdAt: import_zod.z.number(),
635
+ lastUsedAt: import_zod.z.number(),
636
+ expiresAt: import_zod.z.number()
637
+ });
638
+ var CreateSessionSchema = SessionSchema.omit({
639
+ id: true,
640
+ createdAt: true,
641
+ lastUsedAt: true
642
+ }).extend({
643
+ createdAt: import_zod.z.number().optional(),
644
+ lastUsedAt: import_zod.z.number().optional(),
645
+ id: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional()
646
+ });
525
647
  // Annotate the CommonJS export names for ESM import in node:
526
648
  0 && (module.exports = {
527
649
  AuthMiddleware,
528
650
  AuthService,
529
- UserRoleSchema,
651
+ CreateSessionSchema,
652
+ CreateUserSchema,
653
+ SessionSchema,
530
654
  UserSchema
531
655
  });
532
656
  //# sourceMappingURL=index.js.map