@flusys/nestjs-auth 1.0.0-beta → 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 (95) hide show
  1. package/README.md +274 -291
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +219 -10
  4. package/cjs/controllers/branch.controller.js +44 -1
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +44 -1
  7. package/cjs/controllers/user-permission.controller.js +40 -25
  8. package/cjs/controllers/user.controller.js +211 -7
  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/index.js +2 -0
  21. package/cjs/interfaces/user-enricher.interface.js +4 -0
  22. package/cjs/modules/auth.module.js +8 -20
  23. package/cjs/services/auth-config.service.js +23 -2
  24. package/cjs/services/auth-datasource.provider.js +5 -3
  25. package/cjs/services/auth-email.service.js +471 -0
  26. package/cjs/services/authentication.service.js +169 -77
  27. package/cjs/services/branch.service.js +6 -5
  28. package/cjs/services/company-selection-session.service.js +1 -1
  29. package/cjs/services/index.js +1 -0
  30. package/cjs/services/user-permission.service.js +0 -2
  31. package/cjs/services/user.service.js +213 -34
  32. package/cjs/validators/password.validator.js +45 -0
  33. package/config/auth.constants.d.ts +3 -0
  34. package/controllers/authentication.controller.d.ts +13 -9
  35. package/controllers/company-selection.controller.d.ts +6 -2
  36. package/controllers/user.controller.d.ts +33 -0
  37. package/dtos/authentication.dto.d.ts +21 -3
  38. package/dtos/company-branch.dto.d.ts +0 -1
  39. package/dtos/company.dto.d.ts +0 -1
  40. package/dtos/user-permission.dto.d.ts +4 -0
  41. package/dtos/user.dto.d.ts +1 -2
  42. package/entities/index.d.ts +7 -3
  43. package/entities/pending-registration.entity.d.ts +17 -0
  44. package/entities/user-company-permission.entity.d.ts +0 -2
  45. package/entities/user-token.entity.d.ts +15 -0
  46. package/fesm/config/auth.constants.js +6 -2
  47. package/fesm/controllers/authentication.controller.js +221 -12
  48. package/fesm/controllers/branch.controller.js +45 -2
  49. package/fesm/controllers/company-selection.controller.js +28 -21
  50. package/fesm/controllers/company.controller.js +45 -2
  51. package/fesm/controllers/user-permission.controller.js +41 -26
  52. package/fesm/controllers/user.controller.js +214 -10
  53. package/fesm/dtos/authentication.dto.js +106 -14
  54. package/fesm/dtos/user-permission.dto.js +28 -0
  55. package/fesm/dtos/user.dto.js +20 -5
  56. package/fesm/entities/index.js +7 -1
  57. package/fesm/entities/pending-registration.entity.js +115 -0
  58. package/fesm/entities/user-company-permission.entity.js +0 -6
  59. package/fesm/entities/user-token.entity.js +113 -0
  60. package/fesm/guards/auth-throttler.guard.js +43 -0
  61. package/fesm/interceptors/clear-token.interceptor.js +2 -5
  62. package/fesm/interceptors/set-token.interceptor.js +37 -30
  63. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  64. package/fesm/interfaces/authentication.interface.js +1 -1
  65. package/fesm/interfaces/index.js +2 -0
  66. package/fesm/interfaces/user-enricher.interface.js +26 -0
  67. package/fesm/modules/auth.module.js +10 -22
  68. package/fesm/services/auth-config.service.js +24 -3
  69. package/fesm/services/auth-datasource.provider.js +6 -4
  70. package/fesm/services/auth-email.service.js +420 -0
  71. package/fesm/services/authentication.service.js +169 -77
  72. package/fesm/services/branch.service.js +6 -5
  73. package/fesm/services/company-selection-session.service.js +1 -1
  74. package/fesm/services/index.js +1 -0
  75. package/fesm/services/user-permission.service.js +1 -3
  76. package/fesm/services/user.service.js +215 -36
  77. package/fesm/validators/password.validator.js +48 -0
  78. package/guards/auth-throttler.guard.d.ts +7 -0
  79. package/interceptors/set-token.interceptor.d.ts +1 -0
  80. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  81. package/interfaces/auth-module-options.interface.d.ts +4 -1
  82. package/interfaces/authentication.interface.d.ts +14 -0
  83. package/interfaces/index.d.ts +2 -0
  84. package/interfaces/user-enricher.interface.d.ts +64 -0
  85. package/interfaces/user.interface.d.ts +1 -0
  86. package/modules/auth.module.d.ts +0 -1
  87. package/package.json +4 -4
  88. package/services/auth-config.service.d.ts +5 -1
  89. package/services/auth-email.service.d.ts +51 -0
  90. package/services/authentication.service.d.ts +16 -11
  91. package/services/company-selection-session.service.d.ts +1 -1
  92. package/services/index.d.ts +1 -0
  93. package/services/user-permission.service.d.ts +0 -1
  94. package/services/user.service.d.ts +19 -3
  95. package/validators/password.validator.d.ts +10 -0
@@ -21,7 +21,8 @@ function _ts_metadata(k, v) {
21
21
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
22
  }
23
23
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
24
- import { IsBoolean, IsEmail, IsObject, IsOptional, IsString } from 'class-validator';
24
+ import { IsBoolean, IsEmail, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
25
+ import { IsStrongPassword } from '../validators/password.validator';
25
26
  export class CreateUserDto {
26
27
  constructor(){
27
28
  _define_property(this, "name", void 0);
@@ -62,10 +63,13 @@ _ts_decorate([
62
63
  ], CreateUserDto.prototype, "email", void 0);
63
64
  _ts_decorate([
64
65
  ApiProperty({
65
- example: 'password123',
66
- description: 'Password'
66
+ example: 'SecureP@ss123',
67
+ description: 'Password (must meet complexity requirements)'
67
68
  }),
68
69
  IsString(),
70
+ IsNotEmpty(),
71
+ IsStrongPassword(),
72
+ MaxLength(128),
69
73
  _ts_metadata("design:type", String)
70
74
  ], CreateUserDto.prototype, "password", void 0);
71
75
  _ts_decorate([
@@ -118,6 +122,7 @@ export class UpdateUserDto {
118
122
  _define_property(this, "phone", void 0);
119
123
  _define_property(this, "email", void 0);
120
124
  _define_property(this, "password", void 0);
125
+ _define_property(this, "oldPassword", void 0);
121
126
  _define_property(this, "profilePictureId", void 0);
122
127
  _define_property(this, "isActive", void 0);
123
128
  _define_property(this, "emailVerified", void 0);
@@ -129,7 +134,7 @@ _ts_decorate([
129
134
  ApiProperty({
130
135
  description: 'User ID'
131
136
  }),
132
- IsString(),
137
+ IsUUID(),
133
138
  _ts_metadata("design:type", String)
134
139
  ], UpdateUserDto.prototype, "id", void 0);
135
140
  _ts_decorate([
@@ -161,12 +166,22 @@ _ts_decorate([
161
166
  ], UpdateUserDto.prototype, "email", void 0);
162
167
  _ts_decorate([
163
168
  ApiPropertyOptional({
164
- description: 'New password'
169
+ description: 'New password (must meet complexity requirements)'
165
170
  }),
166
171
  IsOptional(),
167
172
  IsString(),
173
+ IsStrongPassword(),
174
+ MaxLength(128),
168
175
  _ts_metadata("design:type", String)
169
176
  ], UpdateUserDto.prototype, "password", void 0);
177
+ _ts_decorate([
178
+ ApiPropertyOptional({
179
+ description: 'Current password (required when changing password on own profile)'
180
+ }),
181
+ IsOptional(),
182
+ IsString(),
183
+ _ts_metadata("design:type", String)
184
+ ], UpdateUserDto.prototype, "oldPassword", void 0);
170
185
  _ts_decorate([
171
186
  ApiPropertyOptional({
172
187
  description: 'Profile picture file ID'
@@ -1,13 +1,19 @@
1
1
  export * from './company-branch.entity';
2
2
  export * from './company.entity';
3
+ export * from './pending-registration.entity';
3
4
  export { PermissionType, UserCompanyPermission } from './user-company-permission.entity';
4
5
  export * from './user.entity';
6
+ export * from './user-token.entity';
5
7
  import { CompanyBranch } from './company-branch.entity';
6
8
  import { Company } from './company.entity';
9
+ import { PendingRegistration } from './pending-registration.entity';
7
10
  import { UserCompanyPermission } from './user-company-permission.entity';
8
11
  import { User } from './user.entity';
12
+ import { UserToken } from './user-token.entity';
9
13
  export const CoreEntities = [
10
- User
14
+ User,
15
+ UserToken,
16
+ PendingRegistration
11
17
  ];
12
18
  export const CompanyEntities = [
13
19
  Company,
@@ -0,0 +1,115 @@
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
+ export class PendingRegistration {
25
+ constructor(){
26
+ _define_property(this, "id", void 0);
27
+ _define_property(this, "email", void 0);
28
+ _define_property(this, "name", void 0);
29
+ _define_property(this, "phone", void 0);
30
+ _define_property(this, "passwordHash", void 0);
31
+ _define_property(this, "token", void 0);
32
+ _define_property(this, "expiresAt", void 0);
33
+ _define_property(this, "companyData", void 0);
34
+ _define_property(this, "createdAt", void 0);
35
+ }
36
+ }
37
+ _ts_decorate([
38
+ PrimaryGeneratedColumn('uuid'),
39
+ _ts_metadata("design:type", String)
40
+ ], PendingRegistration.prototype, "id", void 0);
41
+ _ts_decorate([
42
+ Column({
43
+ type: 'varchar',
44
+ length: 255
45
+ }),
46
+ _ts_metadata("design:type", String)
47
+ ], PendingRegistration.prototype, "email", void 0);
48
+ _ts_decorate([
49
+ Column({
50
+ type: 'varchar',
51
+ length: 255,
52
+ nullable: true
53
+ }),
54
+ _ts_metadata("design:type", Object)
55
+ ], PendingRegistration.prototype, "name", void 0);
56
+ _ts_decorate([
57
+ Column({
58
+ type: 'varchar',
59
+ length: 255,
60
+ nullable: true
61
+ }),
62
+ _ts_metadata("design:type", Object)
63
+ ], PendingRegistration.prototype, "phone", void 0);
64
+ _ts_decorate([
65
+ Column({
66
+ type: 'text',
67
+ name: 'passwordHash'
68
+ }),
69
+ _ts_metadata("design:type", String)
70
+ ], PendingRegistration.prototype, "passwordHash", void 0);
71
+ _ts_decorate([
72
+ Column({
73
+ type: 'varchar',
74
+ length: 64
75
+ }),
76
+ _ts_metadata("design:type", String)
77
+ ], PendingRegistration.prototype, "token", void 0);
78
+ _ts_decorate([
79
+ Column({
80
+ type: 'timestamp',
81
+ name: 'expires_at'
82
+ }),
83
+ _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
84
+ ], PendingRegistration.prototype, "expiresAt", void 0);
85
+ _ts_decorate([
86
+ Column({
87
+ type: 'json',
88
+ nullable: true,
89
+ name: 'company_data'
90
+ }),
91
+ _ts_metadata("design:type", Object)
92
+ ], PendingRegistration.prototype, "companyData", void 0);
93
+ _ts_decorate([
94
+ CreateDateColumn({
95
+ type: 'timestamp',
96
+ name: 'created_at'
97
+ }),
98
+ _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
99
+ ], PendingRegistration.prototype, "createdAt", void 0);
100
+ PendingRegistration = _ts_decorate([
101
+ Entity('pending_registrations'),
102
+ Index('IDX_pending_reg_email', [
103
+ 'email'
104
+ ], {
105
+ unique: true
106
+ }),
107
+ Index('IDX_pending_reg_token', [
108
+ 'token'
109
+ ], {
110
+ unique: true
111
+ }),
112
+ Index('IDX_pending_reg_expires', [
113
+ 'expiresAt'
114
+ ])
115
+ ], PendingRegistration);
@@ -27,12 +27,6 @@ import { Company } from './company.entity';
27
27
  import { User } from './user.entity';
28
28
  export { PermissionType };
29
29
  export class UserCompanyPermission {
30
- /* ---------- Helpers ---------- */ isCompanyPermission() {
31
- return this.permissionType === PermissionType.COMPANY;
32
- }
33
- isBranchPermission() {
34
- return this.permissionType === PermissionType.BRANCH;
35
- }
36
30
  constructor(){
37
31
  _define_property(this, "id", void 0);
38
32
  /* ---------- Ownership ---------- */ _define_property(this, "userId", void 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 +1 @@
1
- /** Login Response when company/branch selection is required */ 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 { };