@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.mjs CHANGED
@@ -2,177 +2,10 @@
2
2
  import bcrypt from "bcryptjs";
3
3
  import jwt from "jsonwebtoken";
4
4
  import { v4 as uuidv4 } from "uuid";
5
-
6
- // src/storage/dynamodb.ts
7
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
8
- import {
9
- DynamoDBDocumentClient,
10
- PutCommand,
11
- GetCommand,
12
- UpdateCommand,
13
- DeleteCommand,
14
- QueryCommand
15
- } from "@aws-sdk/lib-dynamodb";
16
- var DynamoDBStorageProvider = class {
17
- constructor(config) {
18
- const dbClient = new DynamoDBClient({ region: config.region });
19
- this.client = DynamoDBDocumentClient.from(dbClient, {});
20
- this.tableName = config.tableName;
21
- }
22
- async createUser(user) {
23
- await this.client.send(
24
- new PutCommand({
25
- TableName: this.tableName,
26
- Item: user,
27
- ConditionExpression: "attribute_not_exists(email)"
28
- })
29
- );
30
- }
31
- async getUserByResetPasswordToken(resetPasswordToken) {
32
- const response = await this.client.send(
33
- new QueryCommand({
34
- TableName: this.tableName,
35
- IndexName: "ResetPasswordTokenIndex",
36
- KeyConditionExpression: "resetPasswordToken = :token",
37
- ExpressionAttributeValues: {
38
- ":token": resetPasswordToken
39
- }
40
- })
41
- );
42
- return response.Items?.[0];
43
- }
44
- async getUserByVerifyEmailToken(verifyEmailToken) {
45
- const response = await this.client.send(
46
- new QueryCommand({
47
- TableName: this.tableName,
48
- IndexName: "VerifyEmailTokenIndex",
49
- KeyConditionExpression: "verificationToken = :token",
50
- ExpressionAttributeValues: {
51
- ":token": verifyEmailToken
52
- }
53
- })
54
- );
55
- return response.Items?.[0];
56
- }
57
- async getUserById(id) {
58
- const response = await this.client.send(
59
- new GetCommand({
60
- TableName: this.tableName,
61
- Key: { id }
62
- })
63
- );
64
- return response.Item;
65
- }
66
- async getUserByEmail(email) {
67
- const response = await this.client.send(
68
- new QueryCommand({
69
- TableName: this.tableName,
70
- IndexName: "EmailIndex",
71
- KeyConditionExpression: "email = :email",
72
- ExpressionAttributeValues: {
73
- ":email": email
74
- }
75
- })
76
- );
77
- return response.Items?.[0];
78
- }
79
- async updateUser(id, updates) {
80
- const toSet = {};
81
- const toRemove = [];
82
- Object.entries(updates).forEach(([key, value]) => {
83
- if (value === void 0) {
84
- toRemove.push(key);
85
- } else {
86
- toSet[key] = value;
87
- }
88
- });
89
- if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
90
- return;
91
- }
92
- const setPart = Object.keys(toSet).length > 0 ? `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}` : "";
93
- const removePart = toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(", ")}` : "";
94
- const updateExpression = [setPart, removePart].filter(Boolean).join(" ");
95
- const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
96
- (acc, key) => ({ ...acc, [`#${key}`]: key }),
97
- {}
98
- );
99
- const expressionAttributeValues = Object.entries(toSet).reduce(
100
- (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
101
- {}
102
- );
103
- await this.client.send(
104
- new UpdateCommand({
105
- TableName: this.tableName,
106
- Key: { id },
107
- UpdateExpression: updateExpression,
108
- ExpressionAttributeNames: expressionAttributeNames,
109
- ...Object.keys(expressionAttributeValues).length > 0 && {
110
- ExpressionAttributeValues: expressionAttributeValues
111
- }
112
- })
113
- );
114
- }
115
- async deleteUser(id) {
116
- await this.client.send(
117
- new DeleteCommand({
118
- TableName: this.tableName,
119
- Key: { id }
120
- })
121
- );
122
- }
123
- };
124
-
125
- // src/storage/index.ts
126
- function createStorageProvider(config) {
127
- switch (config.type) {
128
- case "dynamodb":
129
- return new DynamoDBStorageProvider(config.options);
130
- case "mongodb":
131
- throw new Error("MongoDB storage provider not implemented yet");
132
- case "postgres":
133
- throw new Error("PostgreSQL storage provider not implemented yet");
134
- default:
135
- throw new Error(`Unsupported storage type: ${config.type}`);
136
- }
137
- }
5
+ import crypto from "crypto";
138
6
 
139
7
  // src/email/smtp.ts
140
8
  import nodemailer from "nodemailer";
141
- var SMTPEmailProvider = class {
142
- constructor(from, config, templates) {
143
- this.transporter = nodemailer.createTransport(config);
144
- this.config = { from, templates };
145
- }
146
- async sendVerificationEmail(email, token) {
147
- const { subject, html } = this.config.templates.verification;
148
- await this.transporter.sendMail({
149
- from: this.config.from,
150
- to: email,
151
- subject,
152
- html: html(token)
153
- });
154
- }
155
- async sendPasswordResetEmail(email, token) {
156
- const { subject, html } = this.config.templates.resetPassword;
157
- await this.transporter.sendMail({
158
- from: this.config.from,
159
- to: email,
160
- subject,
161
- html: html(token)
162
- });
163
- }
164
- async verifyConnection() {
165
- try {
166
- await this.transporter.verify();
167
- return true;
168
- } catch (_error) {
169
- return false;
170
- }
171
- }
172
- };
173
-
174
- // src/email/ses.ts
175
- import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
176
9
 
177
10
  // src/email/defaultTemplates.ts
178
11
  var defaultTemplates = {
@@ -231,7 +64,42 @@ var defaultTemplates = {
231
64
  }
232
65
  };
233
66
 
67
+ // src/email/smtp.ts
68
+ var SMTPEmailProvider = class {
69
+ constructor(from, config, templates) {
70
+ this.transporter = nodemailer.createTransport(config);
71
+ this.config = { from, templates: templates ?? defaultTemplates };
72
+ }
73
+ async sendVerificationEmail(email, token) {
74
+ const { subject, html } = this.config.templates.verification;
75
+ await this.transporter.sendMail({
76
+ from: this.config.from,
77
+ to: email,
78
+ subject,
79
+ html: html(token)
80
+ });
81
+ }
82
+ async sendPasswordResetEmail(email, token) {
83
+ const { subject, html } = this.config.templates.resetPassword;
84
+ await this.transporter.sendMail({
85
+ from: this.config.from,
86
+ to: email,
87
+ subject,
88
+ html: html(token)
89
+ });
90
+ }
91
+ async verifyConnection() {
92
+ try {
93
+ await this.transporter.verify();
94
+ return true;
95
+ } catch (_error) {
96
+ return false;
97
+ }
98
+ }
99
+ };
100
+
234
101
  // src/email/ses.ts
102
+ import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
235
103
  var SESEmailProvider = class {
236
104
  constructor(from, config, templates) {
237
105
  this.client = new SESv2Client({
@@ -321,10 +189,10 @@ function createEmailProvider(config) {
321
189
  var AuthService = class {
322
190
  constructor(config) {
323
191
  this.config = config;
324
- this.storage = createStorageProvider(config.storage);
192
+ this.storage = config.storage;
325
193
  this.email = createEmailProvider(config.email);
326
194
  }
327
- generateToken(user) {
195
+ generateAccessToken(user) {
328
196
  return jwt.sign(
329
197
  {
330
198
  id: user.id,
@@ -340,31 +208,100 @@ var AuthService = class {
340
208
  }
341
209
  );
342
210
  }
211
+ generateRefreshToken(sessionId, userId) {
212
+ const payload = {
213
+ sessionId,
214
+ userId,
215
+ type: "refresh"
216
+ };
217
+ return jwt.sign(payload, this.config.jwt.privateKey, {
218
+ algorithm: "RS256",
219
+ expiresIn: this.config.jwt.refreshTokenExpiresIn || "7d",
220
+ keyid: this.config.jwt.keyId
221
+ });
222
+ }
223
+ verifyRefreshToken(token) {
224
+ try {
225
+ const payload = jwt.verify(token, this.config.jwt.publicKey, {
226
+ algorithms: ["RS256"]
227
+ });
228
+ if (payload.type !== "refresh") {
229
+ throw new Error("Invalid token type");
230
+ }
231
+ return payload;
232
+ } catch (_error) {
233
+ throw new Error("Invalid refresh token");
234
+ }
235
+ }
236
+ hashToken(token) {
237
+ return crypto.createHash("sha256").update(token).digest("hex");
238
+ }
239
+ getRefreshTokenExpiryMs() {
240
+ const expiresIn = this.config.jwt.refreshTokenExpiresIn || "7d";
241
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
242
+ if (!match) {
243
+ return 7 * 24 * 60 * 60 * 1e3;
244
+ }
245
+ const value = parseInt(match[1], 10);
246
+ const unit = match[2];
247
+ const multipliers = {
248
+ s: 1e3,
249
+ m: 60 * 1e3,
250
+ h: 60 * 60 * 1e3,
251
+ d: 24 * 60 * 60 * 1e3
252
+ };
253
+ return value * multipliers[unit];
254
+ }
343
255
  async hashPassword(password) {
344
256
  return bcrypt.hash(password, 10);
345
257
  }
346
- async signup(email, password, roles = ["USER"]) {
347
- const existingUser = await this.storage.getUserByEmail(email);
258
+ async createSession(userId, options = {}) {
259
+ const now = Date.now();
260
+ const sessionId = this.config.idGeneration === "uuid" ? uuidv4() : void 0;
261
+ const tempSession = {
262
+ ...sessionId ? { id: sessionId } : {},
263
+ userId,
264
+ refreshTokenHash: "",
265
+ userAgent: options.userAgent,
266
+ ipAddress: options.ipAddress,
267
+ deviceName: options.deviceName,
268
+ createdAt: now,
269
+ lastUsedAt: now,
270
+ expiresAt: now + this.getRefreshTokenExpiryMs()
271
+ };
272
+ const session = await this.storage.createSession(tempSession);
273
+ const refreshToken = this.generateRefreshToken(session.id, userId);
274
+ await this.storage.updateSession(session.id, {
275
+ refreshTokenHash: this.hashToken(refreshToken)
276
+ });
277
+ return {
278
+ session: { ...session, refreshTokenHash: this.hashToken(refreshToken) },
279
+ refreshToken
280
+ };
281
+ }
282
+ async signup(createUserInput, password, options = {}) {
283
+ const existingUser = await this.storage.getUserByEmail(createUserInput.email);
348
284
  if (existingUser) {
349
285
  throw new Error("User already exists");
350
286
  }
351
287
  const verificationToken = uuidv4();
288
+ const id = this.config.idGeneration === "uuid" ? uuidv4() : void 0;
352
289
  const user = {
353
- id: uuidv4(),
354
- email,
290
+ ...createUserInput,
291
+ id,
355
292
  passwordHash: await this.hashPassword(password),
356
- roles,
357
293
  isEmailVerified: false,
358
294
  verificationToken,
359
295
  createdAt: Date.now(),
360
296
  updatedAt: Date.now()
361
297
  };
362
- await this.storage.createUser(user);
363
- await this.email.sendVerificationEmail(email, verificationToken);
364
- const token = this.generateToken(user);
365
- return { user, token };
298
+ const createdUser = await this.storage.createUser(user);
299
+ await this.email.sendVerificationEmail(createUserInput.email, verificationToken);
300
+ const { refreshToken } = await this.createSession(createdUser.id, options);
301
+ const accessToken = this.generateAccessToken(createdUser);
302
+ return { user: createdUser, tokens: { accessToken, refreshToken } };
366
303
  }
367
- async signin(email, password) {
304
+ async signin(email, password, options = {}) {
368
305
  const user = await this.storage.getUserByEmail(email);
369
306
  if (!user) {
370
307
  throw new Error("Invalid credentials");
@@ -376,8 +313,86 @@ var AuthService = class {
376
313
  if (!user.isEmailVerified) {
377
314
  throw new Error("Please verify your email before signing in");
378
315
  }
379
- const token = this.generateToken(user);
380
- return { user, token };
316
+ const { refreshToken } = await this.createSession(user.id, options);
317
+ const accessToken = this.generateAccessToken(user);
318
+ return { user, tokens: { accessToken, refreshToken } };
319
+ }
320
+ async refreshAccessToken(refreshToken) {
321
+ const payload = this.verifyRefreshToken(refreshToken);
322
+ const session = await this.storage.getSessionById(payload.sessionId);
323
+ if (!session) {
324
+ throw new Error("Session not found");
325
+ }
326
+ const tokenHash = this.hashToken(refreshToken);
327
+ if (session.refreshTokenHash !== tokenHash) {
328
+ throw new Error("Invalid refresh token");
329
+ }
330
+ if (session.expiresAt < Date.now()) {
331
+ await this.storage.deleteSession(session.id);
332
+ throw new Error("Session expired");
333
+ }
334
+ const user = await this.storage.getUserById(session.userId);
335
+ if (!user) {
336
+ throw new Error("User not found");
337
+ }
338
+ const newRefreshToken = this.generateRefreshToken(session.id, user.id);
339
+ await this.storage.updateSession(session.id, {
340
+ refreshTokenHash: this.hashToken(newRefreshToken),
341
+ lastUsedAt: Date.now(),
342
+ expiresAt: Date.now() + this.getRefreshTokenExpiryMs()
343
+ });
344
+ const accessToken = this.generateAccessToken(user);
345
+ return { accessToken, refreshToken: newRefreshToken };
346
+ }
347
+ /**
348
+ * Logout from current session only
349
+ */
350
+ async logout(refreshToken) {
351
+ try {
352
+ const payload = this.verifyRefreshToken(refreshToken);
353
+ await this.storage.deleteSession(payload.sessionId);
354
+ } catch {
355
+ }
356
+ }
357
+ /**
358
+ * Revoke a specific session by ID
359
+ */
360
+ async revokeSession(userId, sessionId) {
361
+ const session = await this.storage.getSessionById(sessionId);
362
+ if (!session || session.userId !== userId) {
363
+ throw new Error("Session not found");
364
+ }
365
+ await this.storage.deleteSession(sessionId);
366
+ }
367
+ /**
368
+ * Revoke all sessions for a user (logout from all devices)
369
+ */
370
+ async revokeAllSessions(userId) {
371
+ await this.storage.deleteAllUserSessions(userId);
372
+ }
373
+ /**
374
+ * Get all active sessions for a user
375
+ */
376
+ async getSessions(userId, currentRefreshToken) {
377
+ const sessions = await this.storage.getSessionsByUserId(userId);
378
+ const now = Date.now();
379
+ let currentSessionId;
380
+ if (currentRefreshToken) {
381
+ try {
382
+ const payload = this.verifyRefreshToken(currentRefreshToken);
383
+ currentSessionId = payload.sessionId;
384
+ } catch {
385
+ }
386
+ }
387
+ return sessions.filter((s) => s.expiresAt > now).map((session) => ({
388
+ id: session.id,
389
+ deviceName: session.deviceName,
390
+ userAgent: session.userAgent,
391
+ ipAddress: session.ipAddress,
392
+ createdAt: session.createdAt,
393
+ lastUsedAt: session.lastUsedAt,
394
+ isCurrent: session.id === currentSessionId
395
+ }));
381
396
  }
382
397
  async verifyEmail(token) {
383
398
  const user = await this.storage.getUserByVerifyEmailToken(token);
@@ -420,6 +435,7 @@ var AuthService = class {
420
435
  try {
421
436
  return jwt.verify(token, this.config.jwt.publicKey, { algorithms: ["RS256"] });
422
437
  } catch (_error) {
438
+ console.log("Token verification failed:", _error);
423
439
  throw new Error("Invalid token");
424
440
  }
425
441
  }
@@ -430,6 +446,74 @@ var AuthService = class {
430
446
  }
431
447
  return requiredRoles.some((role) => user.roles.includes(role));
432
448
  }
449
+ // ==================== User Management ====================
450
+ /**
451
+ * Get a user by ID
452
+ */
453
+ async getUserById(id) {
454
+ return this.storage.getUserById(id);
455
+ }
456
+ /**
457
+ * Get a user by email
458
+ */
459
+ async getUserByEmail(email) {
460
+ return this.storage.getUserByEmail(email);
461
+ }
462
+ /**
463
+ * Update a user's profile information.
464
+ * Supports predefined fields (email, firstName, lastName, roles, isEmailVerified)
465
+ * as well as any additional custom fields.
466
+ */
467
+ async updateUser(id, updates) {
468
+ const user = await this.storage.getUserById(id);
469
+ if (!user) {
470
+ throw new Error("User not found");
471
+ }
472
+ if (updates.email !== void 0 && updates.email !== user.email) {
473
+ const existingUser = await this.storage.getUserByEmail(updates.email);
474
+ if (existingUser) {
475
+ throw new Error("Email already in use");
476
+ }
477
+ }
478
+ const { email, firstName, lastName, roles, isEmailVerified, ...additionalFields } = updates;
479
+ const mergedUpdates = {
480
+ updatedAt: Date.now(),
481
+ ...additionalFields
482
+ };
483
+ if (email !== void 0) mergedUpdates.email = email;
484
+ if (firstName !== void 0) mergedUpdates.firstName = firstName;
485
+ if (lastName !== void 0) mergedUpdates.lastName = lastName;
486
+ if (roles !== void 0) mergedUpdates.roles = roles;
487
+ if (isEmailVerified !== void 0) mergedUpdates.isEmailVerified = isEmailVerified;
488
+ await this.storage.updateUser(id, mergedUpdates);
489
+ return { ...user, ...mergedUpdates };
490
+ }
491
+ /**
492
+ * Delete a user and all their sessions
493
+ */
494
+ async deleteUser(id) {
495
+ const user = await this.storage.getUserById(id);
496
+ if (!user) {
497
+ throw new Error("User not found");
498
+ }
499
+ await this.storage.deleteUser(id);
500
+ }
501
+ /**
502
+ * Find users with filtering and pagination
503
+ *
504
+ * @example
505
+ * // Find all admins
506
+ * const { users } = await authService.findUsers({ hasAnyRole: ['ADMIN'] });
507
+ *
508
+ * // Find verified users with email containing '@company.com'
509
+ * const result = await authService.findUsers({
510
+ * emailContains: '@company.com',
511
+ * isEmailVerified: true
512
+ * }, { limit: 20 });
513
+ */
514
+ async findUsers(filter, options) {
515
+ return this.storage.findUsers(filter, options);
516
+ }
433
517
  };
434
518
 
435
519
  // src/middleware/auth.ts
@@ -444,8 +528,7 @@ var AuthMiddleware = class {
444
528
  if (!token) {
445
529
  throw new Error("No token provided");
446
530
  }
447
- const decoded = await this.authService.verifyToken(token);
448
- req.user = decoded;
531
+ req.user = await this.authService.verifyToken(token);
449
532
  next();
450
533
  } catch (_error) {
451
534
  res.status(401).json({ error: "Unauthorized" });
@@ -477,12 +560,13 @@ var AuthMiddleware = class {
477
560
 
478
561
  // src/types/index.ts
479
562
  import { z } from "zod";
480
- var UserRoleSchema = z.enum(["ADMIN", "USER", "GUEST"]);
481
563
  var UserSchema = z.object({
482
- id: z.string(),
564
+ id: z.union([z.string(), z.number()]),
483
565
  email: z.string().email(),
566
+ firstName: z.string().optional(),
567
+ lastName: z.string().optional(),
484
568
  passwordHash: z.string(),
485
- roles: z.array(UserRoleSchema),
569
+ roles: z.array(z.string()),
486
570
  isEmailVerified: z.boolean(),
487
571
  verificationToken: z.string().optional(),
488
572
  resetPasswordToken: z.string().optional(),
@@ -490,10 +574,41 @@ var UserSchema = z.object({
490
574
  createdAt: z.number(),
491
575
  updatedAt: z.number()
492
576
  });
577
+ var CreateUserSchema = UserSchema.omit({
578
+ id: true,
579
+ createdAt: true,
580
+ updatedAt: true
581
+ }).extend({
582
+ createdAt: z.number().optional(),
583
+ updatedAt: z.number().optional(),
584
+ id: z.union([z.string(), z.number()]).optional()
585
+ });
586
+ var SessionSchema = z.object({
587
+ id: z.union([z.string(), z.number()]),
588
+ userId: z.union([z.string(), z.number()]),
589
+ refreshTokenHash: z.string(),
590
+ userAgent: z.string().optional(),
591
+ ipAddress: z.string().optional(),
592
+ deviceName: z.string().optional(),
593
+ createdAt: z.number(),
594
+ lastUsedAt: z.number(),
595
+ expiresAt: z.number()
596
+ });
597
+ var CreateSessionSchema = SessionSchema.omit({
598
+ id: true,
599
+ createdAt: true,
600
+ lastUsedAt: true
601
+ }).extend({
602
+ createdAt: z.number().optional(),
603
+ lastUsedAt: z.number().optional(),
604
+ id: z.union([z.string(), z.number()]).optional()
605
+ });
493
606
  export {
494
607
  AuthMiddleware,
495
608
  AuthService,
496
- UserRoleSchema,
609
+ CreateSessionSchema,
610
+ CreateUserSchema,
611
+ SessionSchema,
497
612
  UserSchema
498
613
  };
499
614
  //# sourceMappingURL=index.mjs.map