@lenne.tech/nest-server 11.13.3 → 11.13.5

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 (52) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +11 -0
  2. package/dist/core/modules/better-auth/better-auth.config.js +6 -1
  3. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  4. package/dist/core/modules/better-auth/core-better-auth-models.d.ts +1 -0
  5. package/dist/core/modules/better-auth/core-better-auth-models.js +4 -0
  6. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
  7. package/dist/core/modules/better-auth/core-better-auth.controller.js +14 -6
  8. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  9. package/dist/core/modules/better-auth/core-better-auth.module.js +44 -19
  10. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  11. package/dist/core/modules/better-auth/core-better-auth.resolver.js +3 -2
  12. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  13. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +2 -0
  14. package/dist/core/modules/better-auth/core-better-auth.service.js +12 -4
  15. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  16. package/dist/core/modules/error-code/error-codes.d.ts +36 -0
  17. package/dist/core/modules/error-code/error-codes.js +32 -0
  18. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  19. package/dist/core/modules/system-setup/core-system-setup.controller.d.ts +12 -0
  20. package/dist/core/modules/system-setup/core-system-setup.controller.js +86 -0
  21. package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -0
  22. package/dist/core/modules/system-setup/core-system-setup.module.d.ts +2 -0
  23. package/dist/core/modules/system-setup/core-system-setup.module.js +22 -0
  24. package/dist/core/modules/system-setup/core-system-setup.module.js.map +1 -0
  25. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +30 -0
  26. package/dist/core/modules/system-setup/core-system-setup.service.js +157 -0
  27. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -0
  28. package/dist/core.module.js +9 -9
  29. package/dist/core.module.js.map +1 -1
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/server/modules/error-code/error-codes.d.ts +4 -0
  34. package/dist/tsconfig.build.tsbuildinfo +1 -1
  35. package/package.json +1 -1
  36. package/src/core/common/interfaces/server-options.interface.ts +86 -7
  37. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +6 -0
  38. package/src/core/modules/better-auth/README.md +32 -0
  39. package/src/core/modules/better-auth/better-auth.config.ts +12 -2
  40. package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
  41. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -16
  42. package/src/core/modules/better-auth/core-better-auth.module.ts +72 -37
  43. package/src/core/modules/better-auth/core-better-auth.resolver.ts +16 -9
  44. package/src/core/modules/better-auth/core-better-auth.service.ts +30 -7
  45. package/src/core/modules/error-code/error-codes.ts +40 -0
  46. package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +107 -0
  47. package/src/core/modules/system-setup/README.md +267 -0
  48. package/src/core/modules/system-setup/core-system-setup.controller.ts +69 -0
  49. package/src/core/modules/system-setup/core-system-setup.module.ts +19 -0
  50. package/src/core/modules/system-setup/core-system-setup.service.ts +226 -0
  51. package/src/core.module.ts +14 -9
  52. package/src/index.ts +8 -0
@@ -0,0 +1,226 @@
1
+ import { ForbiddenException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
2
+ import { InjectConnection } from '@nestjs/mongoose';
3
+ import { isEmail } from 'class-validator';
4
+ import { Connection } from 'mongoose';
5
+
6
+ import { ConfigService } from '../../common/services/config.service';
7
+ import { CoreBetterAuthUserMapper } from '../better-auth/core-better-auth-user.mapper';
8
+ import { CoreBetterAuthService } from '../better-auth/core-better-auth.service';
9
+ import { ErrorCode } from '../error-code/error-codes';
10
+
11
+ /**
12
+ * Input for creating the initial admin user
13
+ */
14
+ export interface SystemSetupInitInput {
15
+ email: string;
16
+ name?: string;
17
+ password: string;
18
+ }
19
+
20
+ /**
21
+ * Response for successful init
22
+ */
23
+ export interface SystemSetupInitResult {
24
+ email: string;
25
+ message: string;
26
+ success: boolean;
27
+ }
28
+
29
+ /**
30
+ * Response for setup status check
31
+ */
32
+ export interface SystemSetupStatus {
33
+ betterAuthEnabled: boolean;
34
+ needsSetup: boolean;
35
+ }
36
+
37
+ /**
38
+ * CoreSystemSetupService provides initial admin creation for fresh deployments.
39
+ *
40
+ * This service allows creating the first admin user when the system has zero users.
41
+ * It bypasses BetterAuth's disableSignUp check by using the internal adapter directly,
42
+ * which is the same approach used by Better-Auth's own admin plugin.
43
+ *
44
+ * Security:
45
+ * - Only works when zero users exist in the database
46
+ * - Once any user exists, the init endpoint is permanently locked
47
+ * - Race conditions handled by MongoDB unique email index
48
+ */
49
+ @Injectable()
50
+ export class CoreSystemSetupService implements OnApplicationBootstrap {
51
+ private readonly logger = new Logger(CoreSystemSetupService.name);
52
+
53
+ constructor(
54
+ @InjectConnection() private readonly connection: Connection,
55
+ private readonly betterAuthService: CoreBetterAuthService,
56
+ private readonly userMapper: CoreBetterAuthUserMapper,
57
+ private readonly configService: ConfigService,
58
+ ) {}
59
+
60
+ /**
61
+ * Automatically create the initial admin on server start if configured via
62
+ * `systemSetup.initialAdmin` in config or ENV variables.
63
+ *
64
+ * Uses OnApplicationBootstrap (not OnModuleInit) to ensure BetterAuth
65
+ * is fully initialized before attempting user creation.
66
+ */
67
+ async onApplicationBootstrap(): Promise<void> {
68
+ const initialAdmin = this.configService.configFastButReadOnly?.systemSetup?.initialAdmin;
69
+
70
+ // No initialAdmin config at all → skip silently
71
+ if (!initialAdmin) {
72
+ return;
73
+ }
74
+
75
+ // Partial credentials → warn and skip
76
+ if (!initialAdmin.email || !initialAdmin.password) {
77
+ const missing = [
78
+ !initialAdmin.email && 'email',
79
+ !initialAdmin.password && 'password',
80
+ ].filter(Boolean).join(', ');
81
+ this.logger.warn(`Incomplete initialAdmin config - missing: ${missing}. Auto-creation skipped.`);
82
+ return;
83
+ }
84
+
85
+ // Validate email format (same validator as @IsEmail() decorator)
86
+ if (!isEmail(initialAdmin.email)) {
87
+ this.logger.warn(`Invalid initialAdmin email format: "${initialAdmin.email}". Auto-creation skipped.`);
88
+ return;
89
+ }
90
+
91
+ // Validate password is not empty/whitespace
92
+ if (!initialAdmin.password.trim()) {
93
+ this.logger.warn('Empty initialAdmin password. Auto-creation skipped.');
94
+ return;
95
+ }
96
+
97
+ const status = await this.getSetupStatus();
98
+ if (!status.needsSetup) {
99
+ return;
100
+ }
101
+
102
+ if (!status.betterAuthEnabled) {
103
+ this.logger.warn('Initial admin auto-creation skipped: BetterAuth not enabled');
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const result = await this.createInitialAdmin({
109
+ email: initialAdmin.email,
110
+ name: initialAdmin.name,
111
+ password: initialAdmin.password,
112
+ });
113
+ this.logger.log(`Auto-created initial admin on startup: ${result.email}`);
114
+ } catch (error) {
115
+ if (error instanceof ForbiddenException) {
116
+ this.logger.log('Initial admin auto-creation skipped (users already exist)');
117
+ } else {
118
+ this.logger.warn(`Initial admin auto-creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Check if the system needs initial setup (zero users)
125
+ */
126
+ async getSetupStatus(): Promise<SystemSetupStatus> {
127
+ const userCount = await this.connection.collection('users').countDocuments({});
128
+ return {
129
+ betterAuthEnabled: this.betterAuthService.isEnabled(),
130
+ needsSetup: userCount === 0,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Create the initial admin user when zero users exist.
136
+ *
137
+ * Uses BetterAuth's internalAdapter to bypass disableSignUp,
138
+ * then syncs to nest-server users collection with admin role.
139
+ */
140
+ async createInitialAdmin(input: SystemSetupInitInput): Promise<SystemSetupInitResult> {
141
+ // Pre-check: only allow when zero users exist
142
+ const userCount = await this.connection.collection('users').countDocuments({});
143
+ if (userCount > 0) {
144
+ throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_NOT_AVAILABLE);
145
+ }
146
+
147
+ // Ensure BetterAuth is enabled
148
+ if (!this.betterAuthService.isEnabled()) {
149
+ throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_BETTERAUTH_REQUIRED);
150
+ }
151
+
152
+ const authInstance = this.betterAuthService.getInstance();
153
+ if (!authInstance) {
154
+ throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_BETTERAUTH_REQUIRED);
155
+ }
156
+
157
+ try {
158
+ // Access BetterAuth internal context (same pattern as core-better-auth-api.middleware.ts)
159
+ const context = await authInstance.$context;
160
+
161
+ // Normalize password for IAM (SHA256 if plain text)
162
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
163
+
164
+ // Create user via internalAdapter (bypasses disableSignUp)
165
+ const iamUser = await context.internalAdapter.createUser({
166
+ email: input.email,
167
+ emailVerified: true,
168
+ name: input.name || input.email.split('@')[0],
169
+ });
170
+
171
+ if (!iamUser) {
172
+ throw new Error('Failed to create IAM user');
173
+ }
174
+
175
+ // Hash password and create credential account
176
+ const hashedPassword = await context.password.hash(normalizedPassword);
177
+ await context.internalAdapter.linkAccount({
178
+ accountId: iamUser.id,
179
+ password: hashedPassword,
180
+ providerId: 'credential',
181
+ userId: iamUser.id,
182
+ });
183
+
184
+ // Sync to nest-server users collection
185
+ const syncedUser = await this.userMapper.linkOrCreateUser({
186
+ email: iamUser.email,
187
+ emailVerified: true,
188
+ id: iamUser.id,
189
+ name: iamUser.name,
190
+ });
191
+
192
+ if (!syncedUser) {
193
+ throw new Error('Failed to sync user to nest-server collection');
194
+ }
195
+
196
+ // Set admin role directly
197
+ await this.connection
198
+ .collection('users')
199
+ .updateOne({ _id: syncedUser._id }, { $set: { roles: ['admin'], updatedAt: new Date() } });
200
+
201
+ // Sync password to Legacy Auth for backwards compatibility
202
+ await this.userMapper.syncPasswordToLegacy(iamUser.id, input.email, input.password);
203
+
204
+ this.logger.log(`Initial admin user created: ${input.email}`);
205
+
206
+ return {
207
+ email: input.email,
208
+ message: 'Initial admin user created successfully',
209
+ success: true,
210
+ };
211
+ } catch (error) {
212
+ // Handle duplicate email (race condition via MongoDB unique index)
213
+ if (error instanceof Error && (error.message?.includes('duplicate key') || error.message?.includes('E11000'))) {
214
+ throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_NOT_AVAILABLE);
215
+ }
216
+
217
+ // Re-throw known exceptions
218
+ if (error instanceof ForbiddenException) {
219
+ throw error;
220
+ }
221
+
222
+ this.logger.error(`System setup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
223
+ throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_NOT_AVAILABLE);
224
+ }
225
+ }
226
+ }
@@ -24,6 +24,7 @@ import { CoreBetterAuthModule } from './core/modules/better-auth/core-better-aut
24
24
  import { CoreBetterAuthService } from './core/modules/better-auth/core-better-auth.service';
25
25
  import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
26
26
  import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
27
+ import { CoreSystemSetupModule } from './core/modules/system-setup/core-system-setup.module';
27
28
 
28
29
  /**
29
30
  * Core module (dynamic)
@@ -143,9 +144,9 @@ export class CoreModule implements NestModule {
143
144
 
144
145
  // Build GraphQL driver configuration based on auth mode
145
146
  const graphQlDriverConfig = isIamOnlyMode
146
- ? (isAutoRegisterDisabledEarly
147
+ ? isAutoRegisterDisabledEarly
147
148
  ? this.buildLazyIamGraphQlDriver(cors, options)
148
- : this.buildIamOnlyGraphQlDriver(cors, options))
149
+ : this.buildIamOnlyGraphQlDriver(cors, options)
149
150
  : this.buildLegacyGraphQlDriver(AuthService, AuthModule, cors, options);
150
151
 
151
152
  const config: IServerOptions = merge(
@@ -261,16 +262,14 @@ export class CoreModule implements NestModule {
261
262
  // Determine if BetterAuth is explicitly disabled
262
263
  // In IAM-only mode: enabled by default (undefined = true), only false or { enabled: false } disables
263
264
  // In Legacy mode: disabled by default (undefined = false), must be explicitly enabled
264
- const isExplicitlyDisabled = betterAuthConfig === false ||
265
- (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled === false);
266
- const isExplicitlyEnabled = betterAuthConfig === true ||
267
- (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled !== false);
265
+ const isExplicitlyDisabled =
266
+ betterAuthConfig === false || (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled === false);
267
+ const isExplicitlyEnabled =
268
+ betterAuthConfig === true || (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled !== false);
268
269
 
269
270
  // IAM-only mode: enabled unless explicitly disabled
270
271
  // Legacy mode: enabled only if explicitly enabled
271
- const isBetterAuthEnabled = isIamOnlyMode
272
- ? !isExplicitlyDisabled
273
- : isExplicitlyEnabled;
272
+ const isBetterAuthEnabled = isIamOnlyMode ? !isExplicitlyDisabled : isExplicitlyEnabled;
274
273
 
275
274
  const isAutoRegister = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === true;
276
275
  // autoRegister: false means the project imports its own BetterAuthModule separately
@@ -304,6 +303,12 @@ export class CoreModule implements NestModule {
304
303
  }
305
304
  }
306
305
 
306
+ // Add CoreSystemSetupModule when BetterAuth is active
307
+ // Enabled by default - disable explicitly via systemSetup: { enabled: false }
308
+ if (isBetterAuthEnabled && config.systemSetup?.enabled !== false) {
309
+ imports.push(CoreSystemSetupModule);
310
+ }
311
+
307
312
  // Set exports
308
313
  const exports: any[] = [ConfigService, EmailService, TemplateService, MailjetService];
309
314
  if (!process.env.VITEST) {
package/src/index.ts CHANGED
@@ -167,6 +167,14 @@ export * from './core/modules/health-check/core-health-check.service';
167
167
 
168
168
  export * from './core/modules/migrate';
169
169
 
170
+ // =====================================================================================================================
171
+ // Core - Modules - SystemSetup
172
+ // =====================================================================================================================
173
+
174
+ export * from './core/modules/system-setup/core-system-setup.controller';
175
+ export * from './core/modules/system-setup/core-system-setup.module';
176
+ export * from './core/modules/system-setup/core-system-setup.service';
177
+
170
178
  // =====================================================================================================================
171
179
  // Core - Modules - Tus
172
180
  // =====================================================================================================================