@aranzatech/aranza-auth 0.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 ADDED
@@ -0,0 +1,618 @@
1
+ import { __decorateClass, __decorateParam, AUTH_REPOSITORY, AUTH_MODULE_OPTIONS, AUTH_HOOKS, normalizeIdentifier, resolveRegisterIdentifier, readAccountIdentifier } from './chunk-DKYNHXY2.js';
2
+ export { AUTH_HOOKS, AUTH_MODULE_OPTIONS, AUTH_REPOSITORY } from './chunk-DKYNHXY2.js';
3
+ import { createParamDecorator, Injectable, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Controller, Inject, UnauthorizedException, Module, BadRequestException, NotFoundException } from '@nestjs/common';
4
+ import { JwtModule } from '@nestjs/jwt';
5
+ import { AuthGuard, PassportStrategy, PassportModule } from '@nestjs/passport';
6
+ import * as bcrypt from 'bcryptjs';
7
+ import { createHash, randomBytes } from 'crypto';
8
+ import { Strategy, ExtractJwt } from 'passport-jwt';
9
+ import { IsEmail, IsOptional, IsString, Length, IsNotEmpty, Matches } from 'class-validator';
10
+
11
+ var CurrentUser = createParamDecorator(
12
+ (_data, ctx) => {
13
+ const request = ctx.switchToHttp().getRequest();
14
+ return request.user;
15
+ }
16
+ );
17
+ var JwtAuthGuard = class extends AuthGuard("jwt") {
18
+ };
19
+ JwtAuthGuard = __decorateClass([
20
+ Injectable()
21
+ ], JwtAuthGuard);
22
+
23
+ // src/controllers/auth.controller.ts
24
+ var AuthController = class {
25
+ constructor(authService) {
26
+ this.authService = authService;
27
+ }
28
+ register(dto) {
29
+ return this.authService.register(dto);
30
+ }
31
+ login(dto) {
32
+ return this.authService.login(dto);
33
+ }
34
+ refresh(dto) {
35
+ return this.authService.refresh(dto.refreshToken);
36
+ }
37
+ logout(user) {
38
+ return this.authService.logout(user.sub);
39
+ }
40
+ me(user) {
41
+ return this.authService.me(user.sub);
42
+ }
43
+ verifyEmail(dto) {
44
+ return this.authService.verifyEmail(dto.token);
45
+ }
46
+ forgotPassword(dto) {
47
+ return this.authService.forgotPassword(dto.email);
48
+ }
49
+ resetPassword(dto) {
50
+ return this.authService.resetPassword(dto.token, dto.newPassword);
51
+ }
52
+ };
53
+ __decorateClass([
54
+ Post("register"),
55
+ __decorateParam(0, Body())
56
+ ], AuthController.prototype, "register", 1);
57
+ __decorateClass([
58
+ Post("login"),
59
+ __decorateParam(0, Body())
60
+ ], AuthController.prototype, "login", 1);
61
+ __decorateClass([
62
+ Post("refresh"),
63
+ HttpCode(HttpStatus.OK),
64
+ __decorateParam(0, Body())
65
+ ], AuthController.prototype, "refresh", 1);
66
+ __decorateClass([
67
+ Post("logout"),
68
+ UseGuards(JwtAuthGuard),
69
+ HttpCode(HttpStatus.OK),
70
+ __decorateParam(0, CurrentUser())
71
+ ], AuthController.prototype, "logout", 1);
72
+ __decorateClass([
73
+ Get("me"),
74
+ UseGuards(JwtAuthGuard),
75
+ __decorateParam(0, CurrentUser())
76
+ ], AuthController.prototype, "me", 1);
77
+ __decorateClass([
78
+ Post("verify-email"),
79
+ HttpCode(HttpStatus.OK),
80
+ __decorateParam(0, Body())
81
+ ], AuthController.prototype, "verifyEmail", 1);
82
+ __decorateClass([
83
+ Post("forgot-password"),
84
+ HttpCode(HttpStatus.OK),
85
+ __decorateParam(0, Body())
86
+ ], AuthController.prototype, "forgotPassword", 1);
87
+ __decorateClass([
88
+ Post("reset-password"),
89
+ HttpCode(HttpStatus.OK),
90
+ __decorateParam(0, Body())
91
+ ], AuthController.prototype, "resetPassword", 1);
92
+ AuthController = __decorateClass([
93
+ Controller("auth")
94
+ ], AuthController);
95
+ var DefaultAuthHooks = class {
96
+ async buildJwtPayload(account) {
97
+ return {
98
+ sub: account.id,
99
+ ...account.email != null ? { email: account.email } : {},
100
+ ...account.username != null ? { username: account.username } : {}
101
+ };
102
+ }
103
+ async enrichMe(account) {
104
+ return {
105
+ id: account.id,
106
+ email: account.email,
107
+ username: account.username,
108
+ emailVerified: account.emailVerified,
109
+ disabled: account.disabled
110
+ };
111
+ }
112
+ async onBeforeRegister(_input) {
113
+ return;
114
+ }
115
+ async onAfterRegister(_account) {
116
+ return;
117
+ }
118
+ async onAfterLogin(_account) {
119
+ return;
120
+ }
121
+ };
122
+ DefaultAuthHooks = __decorateClass([
123
+ Injectable()
124
+ ], DefaultAuthHooks);
125
+
126
+ // src/utils/duplicate-key.util.ts
127
+ function isDuplicateKeyError(error) {
128
+ return !!error && typeof error === "object" && "code" in error && error.code === 11e3;
129
+ }
130
+ function generateRawToken(byteLength = 32) {
131
+ return randomBytes(byteLength).toString("hex");
132
+ }
133
+ function hashToken(token) {
134
+ return createHash("sha256").update(token).digest("hex");
135
+ }
136
+ function expiresAtFromTtlMs(ttlMs) {
137
+ return new Date(Date.now() + ttlMs);
138
+ }
139
+ var DEFAULT_EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1e3;
140
+ var DEFAULT_PASSWORD_RESET_TTL_MS = 15 * 60 * 1e3;
141
+
142
+ // src/services/auth.service.ts
143
+ var AuthService = class {
144
+ constructor(authRepository, options, hooks, tokenService) {
145
+ this.authRepository = authRepository;
146
+ this.options = options;
147
+ this.hooks = hooks;
148
+ this.tokenService = tokenService;
149
+ }
150
+ get identifierField() {
151
+ return this.options.identifierField ?? "email";
152
+ }
153
+ get emailVerificationEnabled() {
154
+ return this.options.features?.emailVerification === true;
155
+ }
156
+ get passwordResetEnabled() {
157
+ return this.options.features?.passwordReset === true;
158
+ }
159
+ get rotateRefreshToken() {
160
+ return this.options.features?.refreshTokenRotation !== false;
161
+ }
162
+ resolveLoginIdentifier(dto) {
163
+ const value = this.identifierField === "email" ? dto.email : dto.username;
164
+ if (value == null || value.trim() === "") {
165
+ throw new BadRequestException(
166
+ `${this.identifierField} is required for login`
167
+ );
168
+ }
169
+ return normalizeIdentifier(value);
170
+ }
171
+ async register(dto) {
172
+ this.assertEmailHookWhenVerificationEnabled();
173
+ const input = { password: dto.password };
174
+ if (dto.email != null) input.email = dto.email;
175
+ if (dto.username != null) input.username = dto.username;
176
+ await this.hooks.onBeforeRegister?.(input);
177
+ resolveRegisterIdentifier(input, this.identifierField);
178
+ this.assertRegisterEmailWhenVerificationEnabled(input);
179
+ const passwordHash = await bcrypt.hash(dto.password, 10);
180
+ try {
181
+ const account = await this.authRepository.create({
182
+ ...input,
183
+ passwordHash,
184
+ emailVerified: !this.emailVerificationEnabled
185
+ });
186
+ await this.hooks.onAfterRegister?.(account);
187
+ if (this.emailVerificationEnabled) {
188
+ await this.sendVerificationEmail(account);
189
+ }
190
+ } catch (error) {
191
+ if (isDuplicateKeyError(error)) {
192
+ throw new UnauthorizedException(`${this.identifierField} already exists`);
193
+ }
194
+ throw error;
195
+ }
196
+ return { registered: true };
197
+ }
198
+ async login(dto) {
199
+ const identifier = this.resolveLoginIdentifier(dto);
200
+ const account = await this.authRepository.findByIdentifierWithSecrets(
201
+ identifier
202
+ );
203
+ if (account?.passwordHash == null) {
204
+ throw new UnauthorizedException("Invalid credentials");
205
+ }
206
+ this.assertAccountActive(account);
207
+ const passwordMatches = await bcrypt.compare(
208
+ dto.password,
209
+ account.passwordHash
210
+ );
211
+ if (!passwordMatches) {
212
+ throw new UnauthorizedException("Invalid credentials");
213
+ }
214
+ return this.issueTokens(account);
215
+ }
216
+ async refresh(refreshToken) {
217
+ let payload;
218
+ try {
219
+ payload = await this.tokenService.verifyRefreshToken(refreshToken);
220
+ } catch {
221
+ throw new UnauthorizedException("Invalid refresh token");
222
+ }
223
+ const account = await this.authRepository.findByIdWithSecrets(payload.sub);
224
+ if (account?.refreshTokenHash == null) {
225
+ throw new UnauthorizedException("Invalid refresh token");
226
+ }
227
+ this.assertAccountActive(account);
228
+ const tokenMatches = await this.tokenService.compareRefreshToken(
229
+ refreshToken,
230
+ account.refreshTokenHash
231
+ );
232
+ if (!tokenMatches) {
233
+ throw new UnauthorizedException("Invalid refresh token");
234
+ }
235
+ return this.issueTokens(account);
236
+ }
237
+ async logout(authId) {
238
+ await this.authRepository.updateRefreshTokenHash(authId, null);
239
+ return { loggedOut: true };
240
+ }
241
+ async me(authId) {
242
+ const account = await this.authRepository.findById(authId);
243
+ if (account == null) {
244
+ throw new UnauthorizedException("Account not found");
245
+ }
246
+ if (this.hooks.enrichMe != null) {
247
+ return this.hooks.enrichMe(account);
248
+ }
249
+ return new DefaultAuthHooks().enrichMe(account);
250
+ }
251
+ async verifyEmail(token) {
252
+ this.assertEmailVerificationEnabled();
253
+ const tokenHash = hashToken(token);
254
+ const account = await this.authRepository.findByEmailVerificationTokenHash(tokenHash);
255
+ if (account == null) {
256
+ throw new BadRequestException("TOKEN_INVALID_OR_EXPIRED");
257
+ }
258
+ await this.authRepository.markEmailVerified(account.id);
259
+ return { verified: true };
260
+ }
261
+ async forgotPassword(email) {
262
+ this.assertPasswordResetEnabled();
263
+ this.assertEmailHookWhenPasswordResetEnabled();
264
+ const normalizedEmail = normalizeIdentifier(email);
265
+ const account = await this.authRepository.findByEmail(normalizedEmail);
266
+ if (account != null) {
267
+ const rawToken = generateRawToken();
268
+ const tokenHash = hashToken(rawToken);
269
+ const expiresAt = expiresAtFromTtlMs(
270
+ this.options.passwordResetTokenTtlMs ?? DEFAULT_PASSWORD_RESET_TTL_MS
271
+ );
272
+ await this.authRepository.setResetToken(account.id, tokenHash, expiresAt);
273
+ await this.hooks.sendEmail("reset", normalizedEmail, rawToken);
274
+ }
275
+ return { sent: true };
276
+ }
277
+ async resetPassword(token, newPassword) {
278
+ this.assertPasswordResetEnabled();
279
+ const tokenHash = hashToken(token);
280
+ const account = await this.authRepository.findByResetTokenHash(tokenHash);
281
+ if (account == null) {
282
+ throw new BadRequestException("TOKEN_INVALID_OR_EXPIRED");
283
+ }
284
+ const passwordHash = await bcrypt.hash(newPassword, 10);
285
+ await this.authRepository.updatePasswordHash(account.id, passwordHash);
286
+ await this.authRepository.clearResetToken(account.id);
287
+ await this.authRepository.updateRefreshTokenHash(account.id, null);
288
+ return { reset: true };
289
+ }
290
+ assertAccountActive(account) {
291
+ if (account.disabled) {
292
+ throw new UnauthorizedException("ACCOUNT_DISABLED");
293
+ }
294
+ if (this.emailVerificationEnabled && !account.emailVerified) {
295
+ throw new UnauthorizedException("EMAIL_NOT_VERIFIED");
296
+ }
297
+ }
298
+ async issueTokens(account) {
299
+ const payload = await this.hooks.buildJwtPayload(account);
300
+ const tokens = await this.tokenService.signTokens({
301
+ ...payload,
302
+ sub: account.id
303
+ });
304
+ if (this.rotateRefreshToken) {
305
+ const refreshTokenHash = await this.tokenService.hashRefreshToken(
306
+ tokens.refreshToken
307
+ );
308
+ await this.authRepository.updateRefreshTokenHash(
309
+ account.id,
310
+ refreshTokenHash
311
+ );
312
+ }
313
+ await this.hooks.onAfterLogin?.(account);
314
+ return tokens;
315
+ }
316
+ async sendVerificationEmail(account) {
317
+ const email = this.resolveAccountEmail(account);
318
+ if (email == null) return;
319
+ const rawToken = generateRawToken();
320
+ const tokenHash = hashToken(rawToken);
321
+ const expiresAt = expiresAtFromTtlMs(
322
+ this.options.emailVerificationTokenTtlMs ?? DEFAULT_EMAIL_VERIFICATION_TTL_MS
323
+ );
324
+ await this.authRepository.setEmailVerificationToken(
325
+ account.id,
326
+ tokenHash,
327
+ expiresAt
328
+ );
329
+ await this.hooks.sendEmail("verify", email, rawToken);
330
+ }
331
+ resolveAccountEmail(account) {
332
+ if (account.email != null && account.email.trim() !== "") {
333
+ return normalizeIdentifier(account.email);
334
+ }
335
+ return null;
336
+ }
337
+ assertRegisterEmailWhenVerificationEnabled(input) {
338
+ if (!this.emailVerificationEnabled) return;
339
+ const email = this.identifierField === "email" ? resolveRegisterIdentifier(input, "email") : input.email != null ? normalizeIdentifier(input.email) : null;
340
+ if (email == null || email.trim() === "") {
341
+ throw new BadRequestException(
342
+ "email is required when emailVerification feature is enabled"
343
+ );
344
+ }
345
+ }
346
+ assertEmailHookWhenVerificationEnabled() {
347
+ if (this.emailVerificationEnabled && this.hooks.sendEmail == null) {
348
+ throw new BadRequestException(
349
+ "emailVerification is enabled but AuthHooks.sendEmail is not implemented"
350
+ );
351
+ }
352
+ }
353
+ assertEmailHookWhenPasswordResetEnabled() {
354
+ if (this.passwordResetEnabled && this.hooks.sendEmail == null) {
355
+ throw new BadRequestException(
356
+ "passwordReset is enabled but AuthHooks.sendEmail is not implemented"
357
+ );
358
+ }
359
+ }
360
+ assertEmailVerificationEnabled() {
361
+ if (!this.emailVerificationEnabled) {
362
+ throw new NotFoundException();
363
+ }
364
+ }
365
+ assertPasswordResetEnabled() {
366
+ if (!this.passwordResetEnabled) {
367
+ throw new NotFoundException();
368
+ }
369
+ }
370
+ getIdentifierForAccount(account) {
371
+ return readAccountIdentifier(account, this.identifierField);
372
+ }
373
+ };
374
+ AuthService = __decorateClass([
375
+ Injectable(),
376
+ __decorateParam(0, Inject(AUTH_REPOSITORY)),
377
+ __decorateParam(1, Inject(AUTH_MODULE_OPTIONS)),
378
+ __decorateParam(2, Inject(AUTH_HOOKS))
379
+ ], AuthService);
380
+ var TokenService = class {
381
+ constructor(jwtService, options) {
382
+ this.jwtService = jwtService;
383
+ this.options = options;
384
+ }
385
+ async signTokens(payload) {
386
+ const accessExpiresIn = this.options.expiresIn ?? "1h";
387
+ const refreshExpiresIn = this.options.refreshExpiresIn ?? "7d";
388
+ const [accessToken, refreshToken] = await Promise.all([
389
+ this.jwtService.signAsync(
390
+ payload,
391
+ {
392
+ secret: this.options.secret,
393
+ expiresIn: accessExpiresIn
394
+ }
395
+ ),
396
+ this.jwtService.signAsync(
397
+ payload,
398
+ {
399
+ secret: this.options.refreshSecret,
400
+ expiresIn: refreshExpiresIn
401
+ }
402
+ )
403
+ ]);
404
+ return { accessToken, refreshToken };
405
+ }
406
+ async verifyRefreshToken(refreshToken) {
407
+ return this.jwtService.verifyAsync(refreshToken, {
408
+ secret: this.options.refreshSecret
409
+ });
410
+ }
411
+ async hashRefreshToken(refreshToken) {
412
+ return bcrypt.hash(refreshToken, 10);
413
+ }
414
+ async compareRefreshToken(refreshToken, hash3) {
415
+ return bcrypt.compare(refreshToken, hash3);
416
+ }
417
+ };
418
+ TokenService = __decorateClass([
419
+ Injectable(),
420
+ __decorateParam(1, Inject(AUTH_MODULE_OPTIONS))
421
+ ], TokenService);
422
+ var JwtStrategy = class extends PassportStrategy(Strategy) {
423
+ constructor(options, authRepository) {
424
+ super({
425
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
426
+ ignoreExpiration: false,
427
+ secretOrKey: options.secret
428
+ });
429
+ this.authRepository = authRepository;
430
+ }
431
+ async validate(payload) {
432
+ const account = await this.authRepository.findById(payload.sub);
433
+ if (account == null || account.disabled) {
434
+ throw new UnauthorizedException("Account not found or inactive");
435
+ }
436
+ return payload;
437
+ }
438
+ };
439
+ JwtStrategy = __decorateClass([
440
+ Injectable(),
441
+ __decorateParam(0, Inject(AUTH_MODULE_OPTIONS)),
442
+ __decorateParam(1, Inject(AUTH_REPOSITORY))
443
+ ], JwtStrategy);
444
+
445
+ // src/auth.module.ts
446
+ function createAuthProviders(options) {
447
+ return [
448
+ {
449
+ provide: AUTH_MODULE_OPTIONS,
450
+ useValue: options
451
+ },
452
+ {
453
+ provide: AUTH_HOOKS,
454
+ inject: [AUTH_MODULE_OPTIONS],
455
+ useFactory: (opts) => {
456
+ const HooksClass = opts.hooks ?? DefaultAuthHooks;
457
+ return new HooksClass();
458
+ }
459
+ },
460
+ AuthService,
461
+ TokenService,
462
+ JwtStrategy,
463
+ JwtAuthGuard
464
+ ];
465
+ }
466
+ function createAuthImports() {
467
+ return [
468
+ PassportModule.register({ defaultStrategy: "jwt" }),
469
+ JwtModule.registerAsync({
470
+ inject: [AUTH_MODULE_OPTIONS],
471
+ useFactory: (opts) => ({
472
+ secret: opts.secret,
473
+ signOptions: { expiresIn: opts.expiresIn ?? "1h" }
474
+ })
475
+ })
476
+ ];
477
+ }
478
+ function mergeImports(userImports) {
479
+ const merged = [...createAuthImports()];
480
+ if (userImports != null) {
481
+ merged.unshift(...userImports);
482
+ }
483
+ return merged;
484
+ }
485
+ var AuthModule = class {
486
+ static forRoot(options) {
487
+ return {
488
+ module: AuthModule,
489
+ global: true,
490
+ imports: createAuthImports(),
491
+ controllers: [AuthController],
492
+ providers: createAuthProviders(options),
493
+ exports: [
494
+ AUTH_MODULE_OPTIONS,
495
+ AUTH_HOOKS,
496
+ AuthService,
497
+ TokenService,
498
+ JwtAuthGuard,
499
+ JwtModule,
500
+ PassportModule
501
+ ]
502
+ };
503
+ }
504
+ static forRootAsync(options) {
505
+ return {
506
+ module: AuthModule,
507
+ global: true,
508
+ imports: mergeImports(options.imports),
509
+ controllers: [AuthController],
510
+ providers: [
511
+ {
512
+ provide: AUTH_MODULE_OPTIONS,
513
+ inject: options.inject ?? [],
514
+ useFactory: options.useFactory
515
+ },
516
+ {
517
+ provide: AUTH_HOOKS,
518
+ inject: [AUTH_MODULE_OPTIONS],
519
+ useFactory: (opts) => {
520
+ const HooksClass = opts.hooks ?? DefaultAuthHooks;
521
+ return new HooksClass();
522
+ }
523
+ },
524
+ AuthService,
525
+ TokenService,
526
+ JwtStrategy,
527
+ JwtAuthGuard
528
+ ],
529
+ exports: [
530
+ AUTH_MODULE_OPTIONS,
531
+ AUTH_HOOKS,
532
+ AuthService,
533
+ TokenService,
534
+ JwtAuthGuard,
535
+ JwtModule,
536
+ PassportModule
537
+ ]
538
+ };
539
+ }
540
+ };
541
+ AuthModule = __decorateClass([
542
+ Module({})
543
+ ], AuthModule);
544
+
545
+ // src/dto/auth-tokens.dto.ts
546
+ var AuthTokensDto = class {
547
+ };
548
+ var ForgotPasswordDto = class {
549
+ };
550
+ __decorateClass([
551
+ IsEmail()
552
+ ], ForgotPasswordDto.prototype, "email", 2);
553
+ var LoginDto = class {
554
+ };
555
+ __decorateClass([
556
+ IsOptional(),
557
+ IsString(),
558
+ Length(3, 255)
559
+ ], LoginDto.prototype, "email", 2);
560
+ __decorateClass([
561
+ IsOptional(),
562
+ IsString(),
563
+ Length(3, 50)
564
+ ], LoginDto.prototype, "username", 2);
565
+ __decorateClass([
566
+ IsString(),
567
+ IsNotEmpty(),
568
+ Length(8, 128)
569
+ ], LoginDto.prototype, "password", 2);
570
+ var RefreshTokenDto = class {
571
+ };
572
+ __decorateClass([
573
+ IsString(),
574
+ IsNotEmpty()
575
+ ], RefreshTokenDto.prototype, "refreshToken", 2);
576
+
577
+ // src/dto/register-ack.dto.ts
578
+ var RegisterAckDto = class {
579
+ };
580
+ var RegisterDto = class {
581
+ };
582
+ __decorateClass([
583
+ IsOptional(),
584
+ IsString(),
585
+ Length(3, 255)
586
+ ], RegisterDto.prototype, "email", 2);
587
+ __decorateClass([
588
+ IsOptional(),
589
+ IsString(),
590
+ Length(3, 50),
591
+ Matches(/^[a-zA-Z0-9._-]+$/)
592
+ ], RegisterDto.prototype, "username", 2);
593
+ __decorateClass([
594
+ IsString(),
595
+ IsNotEmpty(),
596
+ Length(8, 128)
597
+ ], RegisterDto.prototype, "password", 2);
598
+ var ResetPasswordDto = class {
599
+ };
600
+ __decorateClass([
601
+ IsString(),
602
+ IsNotEmpty()
603
+ ], ResetPasswordDto.prototype, "token", 2);
604
+ __decorateClass([
605
+ IsString(),
606
+ IsNotEmpty(),
607
+ Length(8, 128)
608
+ ], ResetPasswordDto.prototype, "newPassword", 2);
609
+ var VerifyEmailDto = class {
610
+ };
611
+ __decorateClass([
612
+ IsString(),
613
+ IsNotEmpty()
614
+ ], VerifyEmailDto.prototype, "token", 2);
615
+
616
+ export { AuthModule, AuthService, AuthTokensDto, CurrentUser, DefaultAuthHooks, ForgotPasswordDto, JwtAuthGuard, LoginDto, RefreshTokenDto, RegisterAckDto, RegisterDto, ResetPasswordDto, TokenService, VerifyEmailDto };
617
+ //# sourceMappingURL=index.js.map
618
+ //# sourceMappingURL=index.js.map