@flusys/nestjs-auth 0.1.0-beta.3 → 1.0.0-rc

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.
Files changed (102) hide show
  1. package/README.md +274 -301
  2. package/cjs/config/auth.constants.js +22 -4
  3. package/cjs/controllers/authentication.controller.js +222 -38
  4. package/cjs/controllers/branch.controller.js +46 -4
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +46 -4
  7. package/cjs/controllers/user-permission.controller.js +40 -37
  8. package/cjs/controllers/user.controller.js +222 -23
  9. package/cjs/dtos/authentication.dto.js +119 -13
  10. package/cjs/dtos/user-permission.dto.js +28 -0
  11. package/cjs/dtos/user.dto.js +19 -4
  12. package/cjs/entities/index.js +5 -1
  13. package/cjs/entities/pending-registration.entity.js +125 -0
  14. package/cjs/entities/user-company-permission.entity.js +0 -6
  15. package/cjs/entities/user-token.entity.js +129 -0
  16. package/cjs/guards/auth-throttler.guard.js +53 -0
  17. package/cjs/interceptors/clear-token.interceptor.js +3 -6
  18. package/cjs/interceptors/set-token.interceptor.js +38 -31
  19. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  20. package/cjs/interfaces/authentication.interface.js +0 -3
  21. package/cjs/interfaces/index.js +2 -0
  22. package/cjs/interfaces/user-enricher.interface.js +4 -0
  23. package/cjs/modules/auth.module.js +8 -20
  24. package/cjs/services/auth-config.service.js +42 -47
  25. package/cjs/services/auth-datasource.provider.js +18 -34
  26. package/cjs/services/auth-email.service.js +471 -0
  27. package/cjs/services/authentication.service.js +240 -153
  28. package/cjs/services/branch.service.js +15 -48
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/company.service.js +7 -36
  31. package/cjs/services/index.js +1 -0
  32. package/cjs/services/user-permission.service.js +9 -29
  33. package/cjs/services/user.service.js +228 -107
  34. package/cjs/strategies/jwt.strategy.js +14 -1
  35. package/cjs/validators/password.validator.js +45 -0
  36. package/config/auth.constants.d.ts +5 -0
  37. package/controllers/authentication.controller.d.ts +13 -9
  38. package/controllers/company-selection.controller.d.ts +6 -2
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +21 -3
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/user-permission.dto.d.ts +4 -0
  44. package/dtos/user.dto.d.ts +1 -2
  45. package/entities/index.d.ts +7 -3
  46. package/entities/pending-registration.entity.d.ts +17 -0
  47. package/entities/user-company-permission.entity.d.ts +0 -2
  48. package/entities/user-token.entity.d.ts +15 -0
  49. package/fesm/config/auth.constants.js +11 -8
  50. package/fesm/controllers/authentication.controller.js +224 -40
  51. package/fesm/controllers/branch.controller.js +49 -7
  52. package/fesm/controllers/company-selection.controller.js +28 -21
  53. package/fesm/controllers/company.controller.js +49 -7
  54. package/fesm/controllers/user-permission.controller.js +41 -38
  55. package/fesm/controllers/user.controller.js +225 -26
  56. package/fesm/dtos/authentication.dto.js +123 -111
  57. package/fesm/dtos/user-permission.dto.js +28 -0
  58. package/fesm/dtos/user.dto.js +20 -5
  59. package/fesm/entities/index.js +7 -1
  60. package/fesm/entities/pending-registration.entity.js +115 -0
  61. package/fesm/entities/user-company-permission.entity.js +0 -6
  62. package/fesm/entities/user-token.entity.js +113 -0
  63. package/fesm/guards/auth-throttler.guard.js +43 -0
  64. package/fesm/interceptors/clear-token.interceptor.js +2 -5
  65. package/fesm/interceptors/set-token.interceptor.js +37 -30
  66. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  67. package/fesm/interfaces/authentication.interface.js +1 -7
  68. package/fesm/interfaces/index.js +2 -0
  69. package/fesm/interfaces/user-enricher.interface.js +26 -0
  70. package/fesm/modules/auth.module.js +10 -22
  71. package/fesm/services/auth-config.service.js +42 -47
  72. package/fesm/services/auth-datasource.provider.js +19 -35
  73. package/fesm/services/auth-email.service.js +420 -0
  74. package/fesm/services/authentication.service.js +238 -151
  75. package/fesm/services/branch.service.js +16 -49
  76. package/fesm/services/company-selection-session.service.js +1 -1
  77. package/fesm/services/company.service.js +8 -37
  78. package/fesm/services/index.js +1 -0
  79. package/fesm/services/user-permission.service.js +10 -30
  80. package/fesm/services/user.service.js +229 -108
  81. package/fesm/strategies/jwt.strategy.js +14 -1
  82. package/fesm/validators/password.validator.js +48 -0
  83. package/guards/auth-throttler.guard.d.ts +7 -0
  84. package/interceptors/set-token.interceptor.d.ts +1 -0
  85. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  86. package/interfaces/auth-module-options.interface.d.ts +4 -1
  87. package/interfaces/authentication.interface.d.ts +14 -0
  88. package/interfaces/index.d.ts +2 -0
  89. package/interfaces/user-enricher.interface.d.ts +64 -0
  90. package/interfaces/user.interface.d.ts +1 -0
  91. package/modules/auth.module.d.ts +0 -1
  92. package/package.json +4 -4
  93. package/services/auth-config.service.d.ts +5 -1
  94. package/services/auth-email.service.d.ts +51 -0
  95. package/services/authentication.service.d.ts +21 -12
  96. package/services/branch.service.d.ts +2 -5
  97. package/services/company-selection-session.service.d.ts +1 -1
  98. package/services/company.service.d.ts +1 -4
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +0 -1
  101. package/services/user.service.d.ts +20 -6
  102. package/validators/password.validator.d.ts +10 -0
@@ -0,0 +1,113 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ function _ts_decorate(decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ }
20
+ function _ts_metadata(k, v) {
21
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
+ }
23
+ import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
24
+ /**
25
+ * Token types for various user actions
26
+ */ export var TokenType = /*#__PURE__*/ function(TokenType) {
27
+ TokenType["PASSWORD_RESET"] = "password_reset";
28
+ TokenType["EMAIL_VERIFICATION"] = "email_verification";
29
+ TokenType["EMAIL_CHANGE"] = "email_change";
30
+ return TokenType;
31
+ }({});
32
+ export class UserToken {
33
+ constructor(){
34
+ _define_property(this, "id", void 0);
35
+ _define_property(this, "userId", void 0);
36
+ _define_property(this, "type", void 0);
37
+ _define_property(this, "token", void 0);
38
+ _define_property(this, "expiresAt", void 0);
39
+ _define_property(this, "usedAt", void 0);
40
+ _define_property(this, "metadata", void 0);
41
+ _define_property(this, "createdAt", void 0);
42
+ }
43
+ }
44
+ _ts_decorate([
45
+ PrimaryGeneratedColumn('uuid'),
46
+ _ts_metadata("design:type", String)
47
+ ], UserToken.prototype, "id", void 0);
48
+ _ts_decorate([
49
+ Column({
50
+ type: 'uuid',
51
+ name: 'user_id'
52
+ }),
53
+ _ts_metadata("design:type", String)
54
+ ], UserToken.prototype, "userId", void 0);
55
+ _ts_decorate([
56
+ Column({
57
+ type: 'varchar',
58
+ length: 32
59
+ }),
60
+ _ts_metadata("design:type", String)
61
+ ], UserToken.prototype, "type", void 0);
62
+ _ts_decorate([
63
+ Column({
64
+ type: 'varchar',
65
+ length: 64
66
+ }),
67
+ _ts_metadata("design:type", String)
68
+ ], UserToken.prototype, "token", void 0);
69
+ _ts_decorate([
70
+ Column({
71
+ type: 'timestamp',
72
+ name: 'expires_at'
73
+ }),
74
+ _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
75
+ ], UserToken.prototype, "expiresAt", void 0);
76
+ _ts_decorate([
77
+ Column({
78
+ type: 'timestamp',
79
+ name: 'used_at',
80
+ nullable: true
81
+ }),
82
+ _ts_metadata("design:type", Object)
83
+ ], UserToken.prototype, "usedAt", void 0);
84
+ _ts_decorate([
85
+ Column({
86
+ type: 'varchar',
87
+ length: 255,
88
+ nullable: true
89
+ }),
90
+ _ts_metadata("design:type", Object)
91
+ ], UserToken.prototype, "metadata", void 0);
92
+ _ts_decorate([
93
+ CreateDateColumn({
94
+ type: 'timestamp',
95
+ name: 'created_at'
96
+ }),
97
+ _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
98
+ ], UserToken.prototype, "createdAt", void 0);
99
+ UserToken = _ts_decorate([
100
+ Entity('user_tokens'),
101
+ Index('IDX_user_token_token', [
102
+ 'token'
103
+ ], {
104
+ unique: true
105
+ }),
106
+ Index('IDX_user_token_user_type', [
107
+ 'userId',
108
+ 'type'
109
+ ]),
110
+ Index('IDX_user_token_expires', [
111
+ 'expiresAt'
112
+ ])
113
+ ], UserToken);
@@ -0,0 +1,43 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { Injectable } from '@nestjs/common';
8
+ import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler';
9
+ export class AuthThrottlerGuard extends ThrottlerGuard {
10
+ /**
11
+ * Generate tracker key combining IP and email for login attempts.
12
+ * This prevents attackers from brute-forcing multiple accounts from the same IP.
13
+ */ async getTracker(req) {
14
+ const ip = req.ip || req.headers?.['x-forwarded-for'] || 'unknown';
15
+ const body = req.body;
16
+ const email = body?.email || '';
17
+ return `auth:${ip}:${email}`;
18
+ }
19
+ /**
20
+ * Custom error message for rate limit exceeded.
21
+ */ throwThrottlingException() {
22
+ throw new ThrottlerException('Too many authentication attempts. Please try again later.');
23
+ }
24
+ /**
25
+ * Skip rate limiting for authenticated users on certain endpoints.
26
+ */ async shouldSkip(context) {
27
+ const request = context.switchToHttp().getRequest();
28
+ const path = request.path || request.url || '';
29
+ // Remove query string for comparison
30
+ const pathOnly = path.split('?')[0];
31
+ // Skip rate limiting for authenticated endpoints (they have their own protection)
32
+ // Use exact match or prefix match to prevent bypass via path manipulation
33
+ const authenticatedEndpoints = [
34
+ '/auth/me',
35
+ '/auth/logout',
36
+ '/auth/change-password'
37
+ ];
38
+ return authenticatedEndpoints.some((endpoint)=>pathOnly === endpoint || pathOnly.startsWith(`${endpoint}/`));
39
+ }
40
+ }
41
+ AuthThrottlerGuard = _ts_decorate([
42
+ Injectable()
43
+ ], AuthThrottlerGuard);
@@ -27,8 +27,8 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { Injectable, Optional } from '@nestjs/common';
29
29
  import { map } from 'rxjs';
30
+ import { DEFAULT_COOKIE_NAME } from '../config';
30
31
  import { AuthConfigService } from '../services/auth-config.service';
31
- const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
32
32
  export class ClearToken {
33
33
  get cookieName() {
34
34
  return this.authConfig?.getRefreshTokenCookieName() ?? DEFAULT_COOKIE_NAME;
@@ -37,14 +37,11 @@ export class ClearToken {
37
37
  const req = context.switchToHttp().getRequest();
38
38
  const res = context.switchToHttp().getResponse();
39
39
  return next.handle().pipe(map((value)=>{
40
- // Clear the refresh token cookie
41
40
  this.clearRefreshTokenCookie(req, res);
42
41
  return value;
43
42
  }));
44
43
  }
45
- /**
46
- * Clear refresh token cookie
47
- */ clearRefreshTokenCookie(req, res) {
44
+ clearRefreshTokenCookie(req, res) {
48
45
  const origin = req.headers.origin || '';
49
46
  const hostname = req.hostname || '';
50
47
  const isHttps = origin.startsWith('https://') || req.secure;
@@ -25,84 +25,90 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { Injectable, Optional, PayloadTooLargeException } from '@nestjs/common';
28
+ import { Inject, Injectable, Optional, PayloadTooLargeException } from '@nestjs/common';
29
29
  import { Buffer } from 'buffer';
30
30
  import { catchError, map, throwError } from 'rxjs';
31
+ import { DEFAULT_COOKIE_NAME, MAX_COOKIE_BYTES } from '../config';
31
32
  import { AuthConfigService } from '../services/auth-config.service';
32
- const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
33
- const MAX_COOKIE_BYTES = 3500;
34
33
  export class SetToken {
35
34
  get cookieName() {
36
35
  return this.authConfig?.getRefreshTokenCookieName() ?? DEFAULT_COOKIE_NAME;
37
36
  }
37
+ /** Convert expiration string (e.g., '7d', '24h') to milliseconds */ getMaxAgeMs() {
38
+ const expiry = this.authConfig?.getRefreshTokenExpiration() ?? '7d';
39
+ const match = expiry.match(/^(\d+)(s|m|h|d|w)$/);
40
+ if (!match) {
41
+ return 7 * 24 * 60 * 60 * 1000; // Default 7 days
42
+ }
43
+ const value = parseInt(match[1], 10);
44
+ const unit = match[2];
45
+ const multipliers = {
46
+ s: 1000,
47
+ m: 60 * 1000,
48
+ h: 60 * 60 * 1000,
49
+ d: 24 * 60 * 60 * 1000,
50
+ w: 7 * 24 * 60 * 60 * 1000
51
+ };
52
+ return value * (multipliers[unit] || multipliers['d']);
53
+ }
38
54
  intercept(context, next) {
39
55
  const req = context.switchToHttp().getRequest();
40
56
  const res = context.switchToHttp().getResponse();
41
57
  return next.handle().pipe(map((value)=>{
42
- // Skip if no refreshToken in response
43
58
  if (!value || !value.refreshToken) {
44
59
  return value;
45
60
  }
46
61
  const isBrowser = this.isBrowserRequest(req);
47
62
  const tokenValue = value.refreshToken;
48
63
  if (isBrowser) {
49
- // Browser: Set cookie and remove from response body
50
64
  this.setRefreshTokenCookie(req, res, tokenValue);
51
- // Return response without refreshToken for browsers
52
65
  const { refreshToken, ...responseWithoutToken } = value;
53
66
  return responseWithoutToken;
54
67
  }
55
- // API/Mobile: Keep refreshToken in response body
56
68
  return value;
57
69
  }), catchError((err)=>throwError(()=>err)));
58
70
  }
59
- /**
60
- * Detect if request is from a browser
61
- * Checks Accept header and custom X-Client-Type header
62
- */ isBrowserRequest(req) {
63
- // Check custom header first (allows explicit client identification)
71
+ isBrowserRequest(req) {
64
72
  const clientType = req.headers['x-client-type'];
65
73
  if (clientType) {
66
74
  return clientType.toLowerCase() === 'browser' || clientType.toLowerCase() === 'web';
67
75
  }
68
- // Check Accept header - browsers typically include text/html
69
76
  const accept = req.headers['accept'] || '';
70
77
  if (accept.includes('text/html')) {
71
78
  return true;
72
79
  }
73
- // Check User-Agent for common browsers
74
80
  const userAgent = req.headers['user-agent'] || '';
75
81
  const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
76
- if (browserPatterns.test(userAgent) && !userAgent.includes('Postman')) {
77
- return true;
78
- }
79
- return false;
82
+ return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
80
83
  }
81
- /**
82
- * Set refresh token in HTTP-only cookie
83
- */ setRefreshTokenCookie(req, res, tokenValue) {
84
- const origin = req.headers.origin || '';
84
+ setRefreshTokenCookie(req, res, tokenValue) {
85
85
  const hostname = req.hostname || '';
86
- const isHttps = origin.startsWith('https://') || req.secure;
87
- let cookieDomain = hostname;
86
+ // Determine if we're in production - always use secure cookies in production
87
+ // This handles TLS-terminating proxies correctly (they set x-forwarded-proto)
88
+ const isProduction = process.env.NODE_ENV === 'production';
89
+ const forwardedProto = req.headers['x-forwarded-proto'];
90
+ const isHttps = isProduction || forwardedProto === 'https' || req.secure;
91
+ // Set cookie domain - only for subdomains with 3+ parts
92
+ // For security, limit to same domain unless explicitly configured
93
+ let cookieDomain;
88
94
  const domainParts = hostname.split('.');
89
- if (domainParts.length > 2) {
95
+ if (domainParts.length > 2 && !hostname.includes('localhost')) {
90
96
  cookieDomain = '.' + domainParts.slice(-2).join('.');
91
97
  }
92
- // Check cookie size
93
- const rawCookieString = `${this.cookieName}=${tokenValue}; HttpOnly; Domain=${cookieDomain}`;
98
+ const rawCookieString = `${this.cookieName}=${tokenValue}; HttpOnly`;
94
99
  const estimatedBytes = Buffer.byteLength(rawCookieString, 'utf8');
95
100
  if (estimatedBytes > MAX_COOKIE_BYTES) {
96
101
  throw new PayloadTooLargeException(`Refresh token too large (≈${estimatedBytes} bytes)`);
97
102
  }
98
- // Set HTTP-only secure cookie
99
103
  res.cookie(this.cookieName, tokenValue, {
100
104
  httpOnly: true,
101
105
  secure: isHttps,
102
106
  sameSite: isHttps ? 'strict' : 'lax',
103
- domain: cookieDomain,
107
+ ...cookieDomain && {
108
+ domain: cookieDomain
109
+ },
104
110
  path: '/',
105
- maxAge: 7 * 24 * 60 * 60 * 1000
111
+ maxAge: this.getMaxAgeMs()
106
112
  });
107
113
  }
108
114
  constructor(authConfig){
@@ -113,6 +119,7 @@ export class SetToken {
113
119
  SetToken = _ts_decorate([
114
120
  Injectable(),
115
121
  _ts_param(0, Optional()),
122
+ _ts_param(0, Inject(AuthConfigService)),
116
123
  _ts_metadata("design:type", Function),
117
124
  _ts_metadata("design:paramtypes", [
118
125
  typeof AuthConfigService === "undefined" ? Object : AuthConfigService
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auth Email Provider Interface
3
+ *
4
+ * This interface defines the contract for email sending in the auth module.
5
+ * It allows auth to work independently of the email module.
6
+ *
7
+ * To use email features in auth:
8
+ * 1. Provide AUTH_EMAIL_PROVIDER in your app module
9
+ * 2. Implement IAuthEmailProvider using EmailSendService from nestjs-email
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // In app.module.ts or a provider file
14
+ * {
15
+ * provide: AUTH_EMAIL_PROVIDER,
16
+ * useFactory: (emailSendService: EmailSendService) => ({
17
+ * sendPasswordResetEmail: async (email, token, resetUrl) => {
18
+ * await emailSendService.sendTemplateEmail({
19
+ * templateSlug: 'password-reset',
20
+ * to: email,
21
+ * variables: { resetUrl, token },
22
+ * });
23
+ * },
24
+ * sendVerificationEmail: async (email, token, verifyUrl) => {
25
+ * await emailSendService.sendTemplateEmail({
26
+ * templateSlug: 'email-verification',
27
+ * to: email,
28
+ * variables: { verifyUrl, token },
29
+ * });
30
+ * },
31
+ * }),
32
+ * inject: [EmailSendService],
33
+ * }
34
+ * ```
35
+ */ /**
36
+ * Injection token for auth email provider
37
+ * Use this to provide the email implementation
38
+ */ export const AUTH_EMAIL_PROVIDER = 'AUTH_EMAIL_PROVIDER';
@@ -1,7 +1 @@
1
- // =============================================================================
2
- // TOKEN INTERFACES
3
- // =============================================================================
4
- /**
5
- * Login Response when company/branch selection is required
6
- * Frontend shows selection UI and calls /select endpoint with chosen company + branch
7
- */ export { };
1
+ /** Login Response when email verification is required */ export { };
@@ -1,5 +1,7 @@
1
1
  export * from './authentication.interface';
2
+ export * from './auth-email-provider.interface';
2
3
  export * from './company-branch.interface';
3
4
  export * from './company.interface';
4
5
  export * from './user.interface';
6
+ export * from './user-enricher.interface';
5
7
  export * from './auth-module-options.interface';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * User Enricher Provider Interface
3
+ *
4
+ * Allows consuming applications to extend user list and profile
5
+ * without modifying the base nestjs-auth package.
6
+ *
7
+ * This follows the Provider Interface Pattern:
8
+ * - nestjs-auth defines the interface (this file)
9
+ * - Consuming apps implement the interface
10
+ * - nestjs-auth optionally injects the implementation
11
+ *
12
+ * @example
13
+ * // In your app module
14
+ * providers: [
15
+ * { provide: USER_ENRICHER, useClass: MyUserEnricher },
16
+ * ]
17
+ *
18
+ * // Your enricher implementation
19
+ * @Injectable()
20
+ * export class MyUserEnricher implements IUserEnricher {
21
+ * async enrichListQuery(query, user) {
22
+ * query.leftJoinAndSelect('user.department', 'department');
23
+ * return { extraFields: ['department.name'] };
24
+ * }
25
+ * }
26
+ */ export { };
@@ -5,10 +5,9 @@ function _ts_decorate(decorators, target, key, desc) {
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
7
  import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
8
- import { Module, Scope } from '@nestjs/common';
8
+ import { Module } from '@nestjs/common';
9
9
  import { JwtModule } from '@nestjs/jwt';
10
10
  import { PassportModule } from '@nestjs/passport';
11
- import { getRepositoryToken } from '@nestjs/typeorm';
12
11
  import { AuthConfigService } from '../services/auth-config.service';
13
12
  import { AuthDataSourceProvider } from '../services/auth-datasource.provider';
14
13
  import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
@@ -17,14 +16,14 @@ import { Company, CompanyBranch, User, UserCompanyPermission } from '../entities
17
16
  import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
18
17
  import { ClearToken } from '../interceptors/clear-token.interceptor';
19
18
  import { SetToken } from '../interceptors/set-token.interceptor';
20
- import { AuthenticationService, BranchService, CompanySelectionSessionService, CompanyService, UserPermissionService, UserService } from '../services';
19
+ import { AuthEmailService, AuthenticationService, BranchService, CompanySelectionSessionService, CompanyService, UserPermissionService, UserService } from '../services';
21
20
  import { JwtStrategy } from '../strategies/jwt.strategy';
22
21
  export class AuthModule {
23
22
  static forRoot(options) {
24
23
  return this.buildDynamicModule(options);
25
24
  }
26
25
  static forRootAsync(asyncOptions) {
27
- const { global = false, includeController = true, bootstrapAppConfig: { enableCompanyFeature } = {}, imports: externalImports = [] } = asyncOptions;
26
+ const { global = false, includeController = true, bootstrapAppConfig: { enableCompanyFeature } = {}, imports: externalImports = [], providers: additionalProviders = [] } = asyncOptions;
28
27
  const asyncProviders = this.createAsyncProviders(asyncOptions);
29
28
  // Build imports
30
29
  const imports = [
@@ -51,6 +50,7 @@ export class AuthModule {
51
50
  // Build providers
52
51
  const providers = [
53
52
  ...asyncProviders,
53
+ ...additionalProviders,
54
54
  // Config & Infrastructure
55
55
  AuthConfigService,
56
56
  AuthDataSourceProvider,
@@ -58,6 +58,7 @@ export class AuthModule {
58
58
  JwtStrategy,
59
59
  JwtAuthGuard,
60
60
  AuthenticationService,
61
+ AuthEmailService,
61
62
  // Interceptors
62
63
  SetToken,
63
64
  ClearToken,
@@ -77,6 +78,7 @@ export class AuthModule {
77
78
  JwtStrategy,
78
79
  JwtAuthGuard,
79
80
  AuthenticationService,
81
+ AuthEmailService,
80
82
  UserService
81
83
  ];
82
84
  if (enableCompanyFeature) {
@@ -92,7 +94,7 @@ export class AuthModule {
92
94
  };
93
95
  }
94
96
  static buildDynamicModule(options) {
95
- const { bootstrapAppConfig: { enableCompanyFeature } = {}, includeController = true, global = false } = options;
97
+ const { bootstrapAppConfig: { enableCompanyFeature } = {}, includeController = true, global = false, providers: additionalProviders = [] } = options;
96
98
  const imports = [
97
99
  PassportModule.register({
98
100
  defaultStrategy: 'jwt'
@@ -118,6 +120,7 @@ export class AuthModule {
118
120
  provide: AUTH_MODULE_OPTIONS,
119
121
  useValue: options
120
122
  },
123
+ ...additionalProviders,
121
124
  // Config & Infrastructure
122
125
  AuthConfigService,
123
126
  AuthDataSourceProvider,
@@ -125,6 +128,7 @@ export class AuthModule {
125
128
  JwtStrategy,
126
129
  JwtAuthGuard,
127
130
  AuthenticationService,
131
+ AuthEmailService,
128
132
  // Interceptors
129
133
  SetToken,
130
134
  ClearToken,
@@ -143,6 +147,7 @@ export class AuthModule {
143
147
  JwtStrategy,
144
148
  JwtAuthGuard,
145
149
  AuthenticationService,
150
+ AuthEmailService,
146
151
  UserService
147
152
  ];
148
153
  if (enableCompanyFeature) {
@@ -223,23 +228,6 @@ export class AuthModule {
223
228
  }
224
229
  ];
225
230
  }
226
- /**
227
- * Create repository providers that use AuthDataSourceProvider
228
- * This replaces TypeOrmModule.forFeature() functionality
229
- * Note: Must be REQUEST-scoped since AuthDataSourceProvider is REQUEST-scoped
230
- */ static getRepositoryProviders(enableCompanyFeature) {
231
- const entities = this.getEntities(enableCompanyFeature);
232
- return entities.map((entity)=>({
233
- provide: getRepositoryToken(entity),
234
- scope: Scope.REQUEST,
235
- useFactory: async (dataSourceProvider)=>{
236
- return await dataSourceProvider.getRepository(entity);
237
- },
238
- inject: [
239
- AuthDataSourceProvider
240
- ]
241
- }));
242
- }
243
231
  static getEntities(enableCompanyFeature = false) {
244
232
  const coreEntities = [
245
233
  User
@@ -25,87 +25,82 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { Inject, Injectable, Optional } from '@nestjs/common';
28
+ import { Inject, Injectable, InternalServerErrorException, Logger, Optional } from '@nestjs/common';
29
29
  import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
30
30
  import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
31
31
  export class AuthConfigService {
32
- // ==================== Database Mode ====================
33
- /**
34
- * Get database mode (single or multi-tenant)
35
- */ getDatabaseMode() {
32
+ // Database Mode
33
+ /** Get database mode (single or multi-tenant) */ getDatabaseMode() {
36
34
  return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
37
35
  }
38
- /**
39
- * Check if running in multi-tenant mode
40
- */ isMultiTenant() {
36
+ /** Check if running in multi-tenant mode */ isMultiTenant() {
41
37
  return this.getDatabaseMode() === 'multi-tenant';
42
38
  }
43
- // ==================== Feature Flags ====================
44
- /**
45
- * Check if company feature is enabled
46
- */ isCompanyFeatureEnabled() {
39
+ // Feature Flags
40
+ /** Check if company feature is enabled */ isCompanyFeatureEnabled() {
47
41
  return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
48
42
  }
49
- // ==================== JWT Configuration ====================
50
- /**
51
- * Get JWT secret key
52
- */ getJwtSecret() {
53
- return this.config.jwtSecret ?? 'default-secret-change-in-production';
43
+ // JWT Configuration
44
+ /** Get JWT secret key */ getJwtSecret() {
45
+ if (!this.config.jwtSecret) {
46
+ throw new InternalServerErrorException('[AuthModule] JWT_SECRET is required - set via environment or module config');
47
+ }
48
+ return this.config.jwtSecret;
54
49
  }
55
- /**
56
- * Get JWT expiration time
57
- */ getJwtExpiration() {
50
+ /** Get JWT expiration time */ getJwtExpiration() {
58
51
  return this.config.jwtExpiration ?? '1h';
59
52
  }
60
- /**
61
- * Get refresh token secret key
62
- */ getRefreshTokenSecret() {
63
- return this.config.refreshTokenSecret ?? this.getJwtSecret();
53
+ /** Get refresh token secret key */ getRefreshTokenSecret() {
54
+ if (this.config.refreshTokenSecret) {
55
+ return this.config.refreshTokenSecret;
56
+ }
57
+ // Production: require separate refresh token secret for security
58
+ if (process.env.NODE_ENV === 'production') {
59
+ throw new InternalServerErrorException('[AuthModule] REFRESH_TOKEN_SECRET is required in production - ' + 'using the same secret for access and refresh tokens is a security risk');
60
+ }
61
+ // Development: warn once about fallback to JWT secret
62
+ if (!this.refreshTokenSecretWarned) {
63
+ this.refreshTokenSecretWarned = true;
64
+ this.logger.warn('REFRESH_TOKEN_SECRET not configured - using JWT_SECRET as fallback. ' + 'For better security, configure a separate refresh token secret.');
65
+ }
66
+ return this.config.jwtSecret ?? this.getJwtSecret();
64
67
  }
65
- /**
66
- * Get refresh token expiration time
67
- */ getRefreshTokenExpiration() {
68
+ /** Get refresh token expiration time */ getRefreshTokenExpiration() {
68
69
  return this.config.refreshTokenExpiration ?? '7d';
69
70
  }
70
- /**
71
- * Get refresh token cookie name
72
- */ getRefreshTokenCookieName() {
71
+ /** Get refresh token cookie name */ getRefreshTokenCookieName() {
73
72
  return this.config.refreshTokenCookieName ?? 'fsn_refresh_token';
74
73
  }
75
- // ==================== Database Configuration ====================
76
- /**
77
- * Get default database config
78
- */ getDefaultDatabaseConfig() {
74
+ // Frontend Configuration
75
+ /** Get frontend URL for email links */ getFrontendUrl() {
76
+ return this.config.frontendUrl;
77
+ }
78
+ // Database Configuration
79
+ /** Get default database config */ getDefaultDatabaseConfig() {
79
80
  return this.config.defaultDatabaseConfig ?? this.config.tenantDefaultDatabaseConfig;
80
81
  }
81
- /**
82
- * Get tenant default database config (for multi-tenant)
83
- */ getTenantDefaultDatabaseConfig() {
82
+ /** Get tenant default database config (for multi-tenant) */ getTenantDefaultDatabaseConfig() {
84
83
  return this.config.tenantDefaultDatabaseConfig;
85
84
  }
86
- /**
87
- * Get registered tenants
88
- */ getTenants() {
85
+ /** Get registered tenants */ getTenants() {
89
86
  return this.config.tenants ?? [];
90
87
  }
91
- // ==================== Raw Options ====================
92
- /**
93
- * Get full module options
94
- */ getOptions() {
88
+ // Raw Options
89
+ /** Get full module options */ getOptions() {
95
90
  return {
96
91
  ...this.options
97
92
  };
98
93
  }
99
- /**
100
- * Get runtime config
101
- */ getConfig() {
94
+ /** Get runtime config */ getConfig() {
102
95
  return {
103
96
  ...this.config
104
97
  };
105
98
  }
106
99
  constructor(injectedOptions){
100
+ _define_property(this, "logger", new Logger(AuthConfigService.name));
107
101
  _define_property(this, "options", void 0);
108
102
  _define_property(this, "config", void 0);
103
+ _define_property(this, "refreshTokenSecretWarned", false);
109
104
  this.options = injectedOptions ?? {
110
105
  global: false,
111
106
  includeController: true