@flusys/nestjs-auth 1.0.0-beta → 1.0.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.
Files changed (106) hide show
  1. package/README.md +590 -473
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +365 -24
  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 +82 -154
  8. package/cjs/controllers/user.controller.js +211 -7
  9. package/cjs/docs/auth-swagger.config.js +14 -7
  10. package/cjs/dtos/authentication.dto.js +114 -38
  11. package/cjs/dtos/index.js +0 -1
  12. package/cjs/dtos/user-permission.dto.js +28 -0
  13. package/cjs/dtos/user.dto.js +19 -4
  14. package/cjs/entities/index.js +5 -1
  15. package/cjs/entities/pending-registration.entity.js +125 -0
  16. package/cjs/entities/user-company-permission.entity.js +3 -9
  17. package/cjs/entities/user-token.entity.js +129 -0
  18. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  19. package/cjs/interceptors/set-token.interceptor.js +15 -55
  20. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  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 +65 -143
  24. package/cjs/services/auth-config.service.js +25 -27
  25. package/cjs/services/auth-datasource.provider.js +13 -23
  26. package/cjs/services/auth-email.service.js +456 -0
  27. package/cjs/services/authentication.service.js +297 -349
  28. package/cjs/services/branch.service.js +9 -4
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/services/user-permission.service.js +43 -73
  32. package/cjs/services/user.service.js +184 -34
  33. package/cjs/strategies/jwt.strategy.js +44 -44
  34. package/cjs/validators/password.validator.js +45 -0
  35. package/config/auth.constants.d.ts +3 -0
  36. package/controllers/authentication.controller.d.ts +16 -10
  37. package/controllers/company-selection.controller.d.ts +6 -2
  38. package/controllers/user-permission.controller.d.ts +5 -4
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +20 -6
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/index.d.ts +0 -1
  44. package/dtos/user-permission.dto.d.ts +4 -0
  45. package/dtos/user.dto.d.ts +1 -2
  46. package/entities/index.d.ts +7 -3
  47. package/entities/pending-registration.entity.d.ts +17 -0
  48. package/entities/user-company-permission.entity.d.ts +1 -3
  49. package/entities/user-token.entity.d.ts +15 -0
  50. package/fesm/config/auth.constants.js +6 -2
  51. package/fesm/controllers/authentication.controller.js +368 -27
  52. package/fesm/controllers/branch.controller.js +45 -2
  53. package/fesm/controllers/company-selection.controller.js +28 -21
  54. package/fesm/controllers/company.controller.js +45 -2
  55. package/fesm/controllers/user-permission.controller.js +84 -156
  56. package/fesm/controllers/user.controller.js +214 -10
  57. package/fesm/docs/auth-swagger.config.js +14 -7
  58. package/fesm/dtos/authentication.dto.js +101 -39
  59. package/fesm/dtos/index.js +0 -1
  60. package/fesm/dtos/user-permission.dto.js +28 -0
  61. package/fesm/dtos/user.dto.js +20 -5
  62. package/fesm/entities/index.js +7 -1
  63. package/fesm/entities/pending-registration.entity.js +115 -0
  64. package/fesm/entities/user-company-permission.entity.js +3 -9
  65. package/fesm/entities/user-token.entity.js +113 -0
  66. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  67. package/fesm/interceptors/set-token.interceptor.js +14 -54
  68. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  69. package/fesm/interfaces/authentication.interface.js +1 -1
  70. package/fesm/interfaces/index.js +2 -0
  71. package/fesm/interfaces/user-enricher.interface.js +26 -0
  72. package/fesm/modules/auth.module.js +67 -145
  73. package/fesm/services/auth-config.service.js +26 -28
  74. package/fesm/services/auth-datasource.provider.js +14 -24
  75. package/fesm/services/auth-email.service.js +405 -0
  76. package/fesm/services/authentication.service.js +297 -349
  77. package/fesm/services/branch.service.js +9 -4
  78. package/fesm/services/company-selection-session.service.js +1 -1
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/services/user-permission.service.js +44 -74
  81. package/fesm/services/user.service.js +186 -36
  82. package/fesm/strategies/jwt.strategy.js +44 -44
  83. package/fesm/validators/password.validator.js +48 -0
  84. package/interceptors/clear-token.interceptor.d.ts +0 -1
  85. package/interceptors/set-token.interceptor.d.ts +1 -1
  86. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  87. package/interfaces/auth-module-options.interface.d.ts +4 -1
  88. package/interfaces/authentication.interface.d.ts +13 -3
  89. package/interfaces/index.d.ts +2 -0
  90. package/interfaces/user-enricher.interface.d.ts +64 -0
  91. package/interfaces/user.interface.d.ts +1 -0
  92. package/modules/auth.module.d.ts +6 -3
  93. package/package.json +4 -4
  94. package/services/auth-config.service.d.ts +7 -8
  95. package/services/auth-datasource.provider.d.ts +2 -4
  96. package/services/auth-email.service.d.ts +54 -0
  97. package/services/authentication.service.d.ts +27 -16
  98. package/services/company-selection-session.service.d.ts +1 -1
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +6 -9
  101. package/services/user.service.d.ts +19 -3
  102. package/strategies/jwt.strategy.d.ts +4 -1
  103. package/validators/password.validator.d.ts +10 -0
  104. package/cjs/dtos/user-company-permission.dto.js +0 -110
  105. package/dtos/user-company-permission.dto.d.ts +0 -10
  106. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -25,30 +25,43 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { Inject, Injectable, Optional } from '@nestjs/common';
29
- import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
28
+ import { Inject, Injectable, InternalServerErrorException, Logger, Optional } from '@nestjs/common';
30
29
  import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
30
+ import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
31
31
  export class AuthConfigService {
32
- // Database Mode
33
- /** Get database mode (single or multi-tenant) */ getDatabaseMode() {
32
+ // Database Mode (required by IModuleConfigService)
33
+ getDatabaseMode() {
34
34
  return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
35
35
  }
36
- /** Check if running in multi-tenant mode */ isMultiTenant() {
36
+ isMultiTenant() {
37
37
  return this.getDatabaseMode() === 'multi-tenant';
38
38
  }
39
39
  // Feature Flags
40
- /** Check if company feature is enabled */ isCompanyFeatureEnabled() {
40
+ isCompanyFeatureEnabled() {
41
41
  return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
42
42
  }
43
43
  // JWT Configuration
44
44
  /** Get JWT secret key */ getJwtSecret() {
45
- return this.config.jwtSecret ?? 'default-secret-change-in-production';
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;
46
49
  }
47
50
  /** Get JWT expiration time */ getJwtExpiration() {
48
51
  return this.config.jwtExpiration ?? '1h';
49
52
  }
50
53
  /** Get refresh token secret key */ getRefreshTokenSecret() {
51
- return this.config.refreshTokenSecret ?? this.getJwtSecret();
54
+ if (this.config.refreshTokenSecret) {
55
+ return this.config.refreshTokenSecret;
56
+ }
57
+ if (process.env.NODE_ENV === 'production') {
58
+ throw new InternalServerErrorException('[AuthModule] REFRESH_TOKEN_SECRET is required in production - ' + 'using the same secret for access and refresh tokens is a security risk');
59
+ }
60
+ if (!this.refreshTokenSecretWarned) {
61
+ this.refreshTokenSecretWarned = true;
62
+ this.logger.warn('REFRESH_TOKEN_SECRET not configured - using JWT_SECRET as fallback. ' + 'For better security, configure a separate refresh token secret.');
63
+ }
64
+ return this.getJwtSecret();
52
65
  }
53
66
  /** Get refresh token expiration time */ getRefreshTokenExpiration() {
54
67
  return this.config.refreshTokenExpiration ?? '7d';
@@ -56,30 +69,15 @@ export class AuthConfigService {
56
69
  /** Get refresh token cookie name */ getRefreshTokenCookieName() {
57
70
  return this.config.refreshTokenCookieName ?? 'fsn_refresh_token';
58
71
  }
59
- // Database Configuration
60
- /** Get default database config */ getDefaultDatabaseConfig() {
61
- return this.config.defaultDatabaseConfig ?? this.config.tenantDefaultDatabaseConfig;
62
- }
63
- /** Get tenant default database config (for multi-tenant) */ getTenantDefaultDatabaseConfig() {
64
- return this.config.tenantDefaultDatabaseConfig;
65
- }
66
- /** Get registered tenants */ getTenants() {
67
- return this.config.tenants ?? [];
68
- }
69
- // Raw Options
70
- /** Get full module options */ getOptions() {
71
- return {
72
- ...this.options
73
- };
74
- }
75
- /** Get runtime config */ getConfig() {
76
- return {
77
- ...this.config
78
- };
72
+ // Frontend Configuration
73
+ /** Get frontend URL for email links */ getFrontendUrl() {
74
+ return this.config.frontendUrl;
79
75
  }
80
76
  constructor(injectedOptions){
77
+ _define_property(this, "logger", new Logger(AuthConfigService.name));
81
78
  _define_property(this, "options", void 0);
82
79
  _define_property(this, "config", void 0);
80
+ _define_property(this, "refreshTokenSecretWarned", false);
83
81
  this.options = injectedOptions ?? {
84
82
  global: false,
85
83
  includeController: true
@@ -26,7 +26,7 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
29
- import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
29
+ import { Inject, Injectable, InternalServerErrorException, Logger, Optional, Scope } from '@nestjs/common';
30
30
  import { REQUEST } from '@nestjs/core';
31
31
  import { Request } from 'express';
32
32
  import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
@@ -41,7 +41,7 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
41
41
  const lockPromise = (async ()=>{
42
42
  const config = this.getDefaultDatabaseConfig();
43
43
  if (!config) {
44
- throw new Error('Default database config is not available');
44
+ throw new InternalServerErrorException('Default database config is not available');
45
45
  }
46
46
  const ds = await this.createDataSourceFromConfig(config);
47
47
  AuthDataSourceProvider.singleDataSource = ds;
@@ -90,38 +90,28 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
90
90
  };
91
91
  }
92
92
  // Feature Flags
93
- /** Get global enable company feature flag */ getEnableCompanyFeature() {
94
- return this.authOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
95
- }
96
- /** Get enable company feature for specific tenant. Falls back to global setting. */ getEnableCompanyFeatureForTenant(tenant) {
97
- return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
98
- }
99
- /** Get enable company feature for current request context */ getEnableCompanyFeatureForCurrentTenant() {
100
- return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
93
+ /** Check if company feature is enabled (tenant-specific or global fallback) */ isCompanyFeatureEnabled(tenant) {
94
+ const effectiveTenant = tenant ?? this.getCurrentTenant();
95
+ return effectiveTenant?.enableCompanyFeature ?? this.authOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
101
96
  }
102
97
  // Entity Management
103
- /** Get auth entities based on company feature flag */ async getAuthEntities(enableCompanyFeature) {
104
- const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
105
- const { Company, CompanyBranch, User, UserCompanyPermission } = await import('../entities');
98
+ /** Get auth entities based on company feature flag */ async getAuthEntities() {
99
+ const { Company, CompanyBranch, User, UserCompanyPermission, UserToken, PendingRegistration } = await import('../entities');
106
100
  const coreEntities = [
107
- User
101
+ User,
102
+ UserToken,
103
+ PendingRegistration
108
104
  ];
109
- const companyEntities = enable ? [
105
+ return this.isCompanyFeatureEnabled() ? [
106
+ ...coreEntities,
110
107
  Company,
111
108
  CompanyBranch,
112
109
  UserCompanyPermission
113
- ] : [];
114
- return [
115
- ...coreEntities,
116
- ...companyEntities
117
- ];
110
+ ] : coreEntities;
118
111
  }
119
112
  // Overrides
120
113
  /** Override to dynamically set entities based on tenant config */ async createDataSourceFromConfig(config) {
121
- const currentTenant = this.getCurrentTenant();
122
- const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
123
- const entities = await this.getAuthEntities(enableCompanyFeature);
124
- return super.createDataSourceFromConfig(config, entities);
114
+ return super.createDataSourceFromConfig(config, await this.getAuthEntities());
125
115
  }
126
116
  constructor(authOptions, request){
127
117
  super(AuthDataSourceProvider.buildParentOptions(authOptions), request), _define_property(this, "authOptions", void 0), _define_property(this, "logger", void 0), this.authOptions = authOptions, this.logger = new Logger(AuthDataSourceProvider.name);
@@ -0,0 +1,405 @@
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
+ function _ts_param(paramIndex, decorator) {
24
+ return function(target, key) {
25
+ decorator(target, key, paramIndex);
26
+ };
27
+ }
28
+ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, Optional, Scope } from '@nestjs/common';
29
+ import * as bcrypt from 'bcrypt';
30
+ import * as crypto from 'crypto';
31
+ import { LessThan } from 'typeorm';
32
+ import { BCRYPT_SALT_ROUNDS } from '../config';
33
+ import { PendingRegistration } from '../entities/pending-registration.entity';
34
+ import { TokenType, UserToken } from '../entities/user-token.entity';
35
+ import { User } from '../entities/user.entity';
36
+ import { AUTH_EMAIL_PROVIDER, IAuthEmailProvider } from '../interfaces';
37
+ import { AuthDataSourceProvider } from './auth-datasource.provider';
38
+ export class AuthEmailService {
39
+ isEmailEnabled() {
40
+ return !!this.emailProvider;
41
+ }
42
+ // ============================================
43
+ // Private Helpers
44
+ // ============================================
45
+ async getRepository(entity) {
46
+ if (!this.dataSourceProvider) {
47
+ throw new InternalServerErrorException(`${String(entity)} repository not available`);
48
+ }
49
+ return this.dataSourceProvider.getRepository(entity);
50
+ }
51
+ generateToken() {
52
+ return crypto.randomBytes(32).toString('hex');
53
+ }
54
+ /** Hash token for secure storage - prevents token theft if database is compromised */ hashToken(token) {
55
+ return crypto.createHash('sha256').update(token).digest('hex');
56
+ }
57
+ getErrorMessage(error) {
58
+ return error instanceof Error ? error.message : 'Unknown error';
59
+ }
60
+ assertEmailEnabled() {
61
+ if (!this.emailProvider) {
62
+ throw new BadRequestException('Email service is not configured');
63
+ }
64
+ }
65
+ async sendVerificationEmailSafe(email, token, verifyUrl, logContext) {
66
+ try {
67
+ await this.emailProvider.sendVerificationEmail(email, token, verifyUrl);
68
+ this.logger.log(`${logContext} verification email sent to: ${email}`);
69
+ } catch (error) {
70
+ this.logger.error(`Failed to send verification email: ${this.getErrorMessage(error)}`);
71
+ throw new BadRequestException('Failed to send verification email. Please try again.');
72
+ }
73
+ }
74
+ validateTokenNotExpiredOrUsed(token, expiredMessage, usedMessage) {
75
+ if (token.expiresAt < new Date()) {
76
+ throw new BadRequestException(expiredMessage);
77
+ }
78
+ if (usedMessage && token.usedAt) {
79
+ throw new BadRequestException(usedMessage);
80
+ }
81
+ }
82
+ // ============================================
83
+ // Pending Registration (New Users)
84
+ // ============================================
85
+ async hasPendingRegistration(email) {
86
+ if (!this.isEmailEnabled()) return false;
87
+ const pendingRepository = await this.getRepository(PendingRegistration);
88
+ const pending = await pendingRepository.findOne({
89
+ where: {
90
+ email
91
+ }
92
+ });
93
+ return !!pending && pending.expiresAt > new Date();
94
+ }
95
+ async createPendingRegistration(data) {
96
+ this.assertEmailEnabled();
97
+ const userRepository = await this.getRepository(User);
98
+ const pendingRepository = await this.getRepository(PendingRegistration);
99
+ // Check if user already exists
100
+ const existingUser = await userRepository.findOne({
101
+ where: {
102
+ email: data.email
103
+ }
104
+ });
105
+ if (existingUser) {
106
+ throw new BadRequestException('Registration failed. Please try again or contact support.');
107
+ }
108
+ // Delete any existing pending registration for this email
109
+ await pendingRepository.delete({
110
+ email: data.email
111
+ });
112
+ // Create pending registration with hashed token for secure storage
113
+ const token = this.generateToken();
114
+ const tokenHash = this.hashToken(token);
115
+ const passwordHash = await bcrypt.hash(data.password, BCRYPT_SALT_ROUNDS);
116
+ const expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
117
+ const pending = pendingRepository.create({
118
+ email: data.email,
119
+ name: data.name ?? null,
120
+ phone: data.phone ?? null,
121
+ passwordHash,
122
+ token: tokenHash,
123
+ expiresAt,
124
+ companyData: data.companyData ?? null
125
+ });
126
+ await pendingRepository.save(pending);
127
+ // Send verification email with raw token (user receives this)
128
+ const verifyUrl = `${data.verifyUrlBase}?token=${token}`;
129
+ await this.sendVerificationEmailSafe(data.email, token, verifyUrl, 'Registration');
130
+ return {
131
+ success: true,
132
+ message: 'Please check your email to verify your registration.'
133
+ };
134
+ }
135
+ // ============================================
136
+ // Password Reset (Existing Users)
137
+ // ============================================
138
+ /**
139
+ * Request password reset - sends reset email
140
+ */ async forgotPassword(email, resetUrlBase) {
141
+ this.assertEmailEnabled();
142
+ const userRepository = await this.getRepository(User);
143
+ const tokenRepository = await this.getRepository(UserToken);
144
+ const user = await userRepository.findOne({
145
+ where: {
146
+ email
147
+ }
148
+ });
149
+ // Always return success to prevent email enumeration
150
+ if (!user || !user.isActive) {
151
+ this.logger.warn(`Password reset requested for non-existent/inactive email: ${email}`);
152
+ return {
153
+ success: true,
154
+ message: 'If the email exists, a password reset link has been sent.'
155
+ };
156
+ }
157
+ // Invalidate existing password reset tokens for this user
158
+ await tokenRepository.delete({
159
+ userId: user.id,
160
+ type: TokenType.PASSWORD_RESET
161
+ });
162
+ // Create new token with hashed storage
163
+ const token = this.generateToken();
164
+ const tokenHash = this.hashToken(token);
165
+ const userToken = tokenRepository.create({
166
+ userId: user.id,
167
+ type: TokenType.PASSWORD_RESET,
168
+ token: tokenHash,
169
+ expiresAt: new Date(Date.now() + this.PASSWORD_RESET_EXPIRY)
170
+ });
171
+ await tokenRepository.save(userToken);
172
+ // Send email with raw token
173
+ const resetUrl = `${resetUrlBase}?token=${token}`;
174
+ try {
175
+ await this.emailProvider.sendPasswordResetEmail(email, token, resetUrl);
176
+ this.logger.log(`Password reset email sent to: ${email}`);
177
+ } catch (error) {
178
+ this.logger.error(`Failed to send password reset email: ${this.getErrorMessage(error)}`);
179
+ throw new BadRequestException('Failed to process request. Please try again later.');
180
+ }
181
+ return {
182
+ success: true,
183
+ message: 'If the email exists, a password reset link has been sent.'
184
+ };
185
+ }
186
+ /**
187
+ * Reset password using token
188
+ */ async resetPassword(token, newPassword) {
189
+ const userRepository = await this.getRepository(User);
190
+ const tokenRepository = await this.getRepository(UserToken);
191
+ // Hash the incoming token to compare with stored hash
192
+ const tokenHash = this.hashToken(token);
193
+ const userToken = await tokenRepository.findOne({
194
+ where: {
195
+ token: tokenHash,
196
+ type: TokenType.PASSWORD_RESET
197
+ }
198
+ });
199
+ if (!userToken) {
200
+ throw new BadRequestException('Invalid or expired reset token');
201
+ }
202
+ this.validateTokenNotExpiredOrUsed(userToken, 'Reset token has expired. Please request a new one.', 'This reset token has already been used.');
203
+ const user = await userRepository.findOne({
204
+ where: {
205
+ id: userToken.userId
206
+ }
207
+ });
208
+ if (!user) {
209
+ throw new BadRequestException('User not found');
210
+ }
211
+ // Update password
212
+ user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
213
+ await userRepository.save(user);
214
+ // Mark token as used
215
+ userToken.usedAt = new Date();
216
+ await tokenRepository.save(userToken);
217
+ this.logger.log(`Password reset successful for user: ${user.email}`);
218
+ return {
219
+ success: true,
220
+ message: 'Password has been reset successfully.'
221
+ };
222
+ }
223
+ // ============================================
224
+ // Email Verification
225
+ // ============================================
226
+ /**
227
+ * Verify email using token
228
+ * Handles both pending registration and existing user verification
229
+ */ async verifyEmail(token) {
230
+ const pendingRepository = await this.getRepository(PendingRegistration);
231
+ const userRepository = await this.getRepository(User);
232
+ const tokenRepository = await this.getRepository(UserToken);
233
+ // Hash the incoming token to compare with stored hash
234
+ const tokenHash = this.hashToken(token);
235
+ // Check for pending registration first
236
+ const pending = await pendingRepository.findOne({
237
+ where: {
238
+ token: tokenHash
239
+ }
240
+ });
241
+ if (pending) {
242
+ if (pending.expiresAt < new Date()) {
243
+ await pendingRepository.delete({
244
+ id: pending.id
245
+ });
246
+ throw new BadRequestException('Verification token has expired. Please register again.');
247
+ }
248
+ // Delete pending registration (will be consumed by caller to create user)
249
+ await pendingRepository.delete({
250
+ id: pending.id
251
+ });
252
+ return {
253
+ type: 'pending',
254
+ pending
255
+ };
256
+ }
257
+ // Check for existing user email verification token
258
+ const userToken = await tokenRepository.findOne({
259
+ where: {
260
+ token: tokenHash,
261
+ type: TokenType.EMAIL_VERIFICATION
262
+ }
263
+ });
264
+ if (!userToken) {
265
+ throw new BadRequestException('Invalid or expired verification token');
266
+ }
267
+ if (userToken.expiresAt < new Date()) {
268
+ await tokenRepository.delete({
269
+ id: userToken.id
270
+ });
271
+ throw new BadRequestException('Verification token has expired. Please request a new one.');
272
+ }
273
+ if (userToken.usedAt) {
274
+ return {
275
+ type: 'verified',
276
+ message: 'Email has already been verified.'
277
+ };
278
+ }
279
+ const user = await userRepository.findOne({
280
+ where: {
281
+ id: userToken.userId
282
+ }
283
+ });
284
+ if (!user) {
285
+ throw new BadRequestException('User not found');
286
+ }
287
+ // Mark email as verified
288
+ user.emailVerified = true;
289
+ await userRepository.save(user);
290
+ // Mark token as used
291
+ userToken.usedAt = new Date();
292
+ await tokenRepository.save(userToken);
293
+ this.logger.log(`Email verified for user: ${user.email}`);
294
+ return {
295
+ type: 'verified',
296
+ message: 'Email has been verified successfully.'
297
+ };
298
+ }
299
+ /**
300
+ * Resend verification email - handles both pending registrations and existing users
301
+ */ async resendVerificationEmail(email, verifyUrlBase) {
302
+ this.assertEmailEnabled();
303
+ const pendingRepository = await this.getRepository(PendingRegistration);
304
+ const userRepository = await this.getRepository(User);
305
+ const tokenRepository = await this.getRepository(UserToken);
306
+ // Check pending registration first
307
+ const pending = await pendingRepository.findOne({
308
+ where: {
309
+ email
310
+ }
311
+ });
312
+ if (pending) {
313
+ const token = this.generateToken();
314
+ pending.token = this.hashToken(token);
315
+ pending.expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
316
+ await pendingRepository.save(pending);
317
+ const verifyUrl = `${verifyUrlBase}?token=${token}`;
318
+ await this.sendVerificationEmailSafe(email, token, verifyUrl, 'Re-sent registration');
319
+ return {
320
+ success: true,
321
+ message: 'If the email requires verification, a link has been sent.'
322
+ };
323
+ }
324
+ // Check existing user
325
+ const user = await userRepository.findOne({
326
+ where: {
327
+ email
328
+ }
329
+ });
330
+ if (!user || user.emailVerified) {
331
+ return {
332
+ success: true,
333
+ message: 'If the email requires verification, a link has been sent.'
334
+ };
335
+ }
336
+ // Invalidate existing tokens and create new one
337
+ await tokenRepository.delete({
338
+ userId: user.id,
339
+ type: TokenType.EMAIL_VERIFICATION
340
+ });
341
+ const token = this.generateToken();
342
+ const userToken = tokenRepository.create({
343
+ userId: user.id,
344
+ type: TokenType.EMAIL_VERIFICATION,
345
+ token: this.hashToken(token),
346
+ expiresAt: new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY)
347
+ });
348
+ await tokenRepository.save(userToken);
349
+ const verifyUrl = `${verifyUrlBase}?token=${token}`;
350
+ await this.sendVerificationEmailSafe(user.email, token, verifyUrl, 'Verification');
351
+ return {
352
+ success: true,
353
+ message: 'If the email requires verification, a link has been sent.'
354
+ };
355
+ }
356
+ // ============================================
357
+ // Cleanup
358
+ // ============================================
359
+ /**
360
+ * Clean up expired tokens and pending registrations
361
+ */ async cleanupExpired() {
362
+ const tokenRepository = await this.getRepository(UserToken);
363
+ const pendingRepository = await this.getRepository(PendingRegistration);
364
+ const now = new Date();
365
+ const tokenResult = await tokenRepository.delete({
366
+ expiresAt: LessThan(now)
367
+ });
368
+ const pendingResult = await pendingRepository.delete({
369
+ expiresAt: LessThan(now)
370
+ });
371
+ const deletedTokens = tokenResult.affected ?? 0;
372
+ const deletedPending = pendingResult.affected ?? 0;
373
+ this.logger.log(`Cleaned up ${deletedTokens} expired tokens and ${deletedPending} expired pending registrations`);
374
+ return {
375
+ deletedTokens,
376
+ deletedPending
377
+ };
378
+ }
379
+ constructor(dataSourceProvider, emailProvider){
380
+ _define_property(this, "dataSourceProvider", void 0);
381
+ _define_property(this, "emailProvider", void 0);
382
+ _define_property(this, "logger", void 0);
383
+ _define_property(this, "PASSWORD_RESET_EXPIRY", void 0); // 1 hour
384
+ _define_property(this, "EMAIL_VERIFICATION_EXPIRY", void 0); // 24 hours
385
+ this.dataSourceProvider = dataSourceProvider;
386
+ this.emailProvider = emailProvider;
387
+ this.logger = new Logger(AuthEmailService.name);
388
+ this.PASSWORD_RESET_EXPIRY = 60 * 60 * 1000;
389
+ this.EMAIL_VERIFICATION_EXPIRY = 24 * 60 * 60 * 1000;
390
+ }
391
+ }
392
+ AuthEmailService = _ts_decorate([
393
+ Injectable({
394
+ scope: Scope.REQUEST
395
+ }),
396
+ _ts_param(0, Optional()),
397
+ _ts_param(0, Inject(AuthDataSourceProvider)),
398
+ _ts_param(1, Optional()),
399
+ _ts_param(1, Inject(AUTH_EMAIL_PROVIDER)),
400
+ _ts_metadata("design:type", Function),
401
+ _ts_metadata("design:paramtypes", [
402
+ typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
403
+ typeof IAuthEmailProvider === "undefined" ? Object : IAuthEmailProvider
404
+ ])
405
+ ], AuthEmailService);