@lenne.tech/nest-server 12.3.2 → 12.4.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 (34) hide show
  1. package/.claude/rules/configurable-features.md +1 -1
  2. package/.claude/rules/testing.md +4 -4
  3. package/FRAMEWORK-API.md +1 -1
  4. package/dist/core/common/helpers/swagger.helper.js +6 -0
  5. package/dist/core/common/helpers/swagger.helper.js.map +1 -1
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
  7. package/dist/core/modules/error-code/error-codes.d.ts +18 -0
  8. package/dist/core/modules/error-code/error-codes.js +16 -0
  9. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  10. package/dist/core/modules/system-setup/core-system-setup.controller.d.ts +2 -2
  11. package/dist/core/modules/system-setup/core-system-setup.controller.js +17 -7
  12. package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -1
  13. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +18 -0
  14. package/dist/core/modules/system-setup/core-system-setup.service.js +118 -15
  15. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  16. package/dist/core/modules/system-setup/system-setup-http-init.helper.d.ts +11 -0
  17. package/dist/core/modules/system-setup/system-setup-http-init.helper.js +45 -0
  18. package/dist/core/modules/system-setup/system-setup-http-init.helper.js.map +1 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/modules/error-code/error-codes.d.ts +2 -0
  23. package/migration-guides/11.13.x-to-11.14.0.md +5 -5
  24. package/migration-guides/12.3.x-to-12.4.x.md +169 -0
  25. package/package.json +1 -1
  26. package/src/core/common/helpers/swagger.helper.ts +7 -0
  27. package/src/core/common/interfaces/server-options.interface.ts +59 -5
  28. package/src/core/modules/error-code/error-codes.ts +27 -0
  29. package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +51 -16
  30. package/src/core/modules/system-setup/README.md +144 -28
  31. package/src/core/modules/system-setup/core-system-setup.controller.ts +30 -8
  32. package/src/core/modules/system-setup/core-system-setup.service.ts +246 -23
  33. package/src/core/modules/system-setup/system-setup-http-init.helper.ts +130 -0
  34. package/src/index.ts +1 -0
@@ -3611,7 +3611,10 @@ export interface IServerOptions {
3611
3611
  *
3612
3612
  * Auto-creation via config/ENV (no REST call needed):
3613
3613
  * - `{ initialAdmin: { email: '...', password: '...' } }`
3614
- * - `NSC__systemSetup__initialAdmin__email` + `NSC__systemSetup__initialAdmin__password`
3614
+ * - `NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL` + `NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD`
3615
+ *
3616
+ * The anonymous `POST /system-setup/init` is open only in `local`, `ci` and `e2e` unless `httpInit` opens it
3617
+ * (12.4.0+, see {@link ISystemSetup.httpInit}).
3615
3618
  *
3616
3619
  * @since 11.14.0
3617
3620
  *
@@ -4048,23 +4051,74 @@ export interface ISystemSetup {
4048
4051
  */
4049
4052
  enabled?: boolean;
4050
4053
 
4054
+ /**
4055
+ * Whether the anonymous `POST /system-setup/init` may create the first administrator.
4056
+ *
4057
+ * Fail-closed outside the local environments: whoever reaches an API without users through that route becomes its
4058
+ * administrator, so a deployment provisions the first admin through {@link initialAdmin} and opens the HTTP route
4059
+ * only on purpose.
4060
+ *
4061
+ * | Value | `local` / `ci` / `e2e` | every other environment (`develop`, `test`, `production`, …) |
4062
+ * |-------|------------------------|---------------------------------------------------------------|
4063
+ * | not set | open | **closed** (403 `LTNS_0053`) |
4064
+ * | `true` | open | open, without a token |
4065
+ * | `{ token: '<at least 16 characters>' }` | open, token required | open, token required |
4066
+ * | `false` / `{ enabled: false, … }` | closed | closed |
4067
+ *
4068
+ * The token is sent in the `X-System-Setup-Token` header; a missing or wrong one answers 403 `LTNS_0054`. An object
4069
+ * whose token is missing, empty, shorter than 16 characters or not a string keeps the route CLOSED and logs an
4070
+ * error at boot — `{ token: process.env.SETUP_TOKEN }` with the variable unset must not open it. A value that is
4071
+ * neither a boolean nor an object (the string `'true'` from `NEST_SERVER_CONFIG`) counts as not set.
4072
+ *
4073
+ * Environment variables: `NSC__SYSTEM_SETUP__HTTP_INIT=true`, or `NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN=<secret>` (a
4074
+ * token of digits only is converted to a number by the environment reader and refused; set one of the two, not
4075
+ * both).
4076
+ *
4077
+ * `GET /system-setup/status` reports the result as `httpInitEnabled` / `httpInitTokenRequired`.
4078
+ *
4079
+ * @default undefined (open only in `local`, `ci` and `e2e`)
4080
+ * @since 12.4.0
4081
+ */
4082
+ httpInit?: boolean | ISystemSetupHttpInit;
4083
+
4051
4084
  /**
4052
4085
  * Pre-configured initial admin credentials for automatic creation on server start.
4053
4086
  *
4054
4087
  * When set, the service will automatically create the initial admin user
4055
4088
  * during application bootstrap if zero users exist. This is useful for
4056
4089
  * automated deployments (Docker, CI/CD) where no manual REST call is possible.
4090
+ * It is not affected by {@link httpInit}.
4057
4091
  *
4058
- * Can be provided via environment variables:
4059
- * - `NSC__systemSetup__initialAdmin__email`
4060
- * - `NSC__systemSetup__initialAdmin__password`
4061
- * - `NSC__systemSetup__initialAdmin__name` (optional)
4092
+ * Can be provided via environment variables (SCREAMING_SNAKE segments — a camelCase segment such as
4093
+ * `NSC__systemSetup__…` is NOT read, the boot warns about it):
4094
+ * - `NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL`
4095
+ * - `NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD`
4096
+ * - `NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME` (optional)
4062
4097
  *
4063
4098
  * Security: Same zero-user guard applies - only works when no users exist.
4064
4099
  */
4065
4100
  initialAdmin?: ISystemSetupInitialAdmin;
4066
4101
  }
4067
4102
 
4103
+ /**
4104
+ * Object form of {@link ISystemSetup.httpInit}: the HTTP init, protected by a setup token
4105
+ *
4106
+ * @since 12.4.0
4107
+ */
4108
+ export interface ISystemSetupHttpInit {
4109
+ /**
4110
+ * `false` keeps the HTTP init closed while the token stays configured
4111
+ * @default true
4112
+ */
4113
+ enabled?: boolean;
4114
+
4115
+ /**
4116
+ * The secret `POST /system-setup/init` must carry in the `X-System-Setup-Token` header. At least 16 characters;
4117
+ * a missing, shorter or non-string value keeps the HTTP init closed.
4118
+ */
4119
+ token?: string;
4120
+ }
4121
+
4068
4122
  /**
4069
4123
  * System setup configuration interface
4070
4124
  *
@@ -637,6 +637,33 @@ export const LtnsErrors = {
637
637
  },
638
638
  },
639
639
 
640
+ /**
641
+ * `POST /system-setup/init` is closed in this environment (12.4.0+): by default it is open only in `local`, `ci`
642
+ * and `e2e`. A deployment provisions its first admin through `systemSetup.initialAdmin`, or opens the HTTP init
643
+ * explicitly with `systemSetup.httpInit`.
644
+ */
645
+ SYSTEM_SETUP_HTTP_INIT_DISABLED: {
646
+ code: 'LTNS_0053',
647
+ message: 'Initial admin setup over HTTP is not enabled in this environment',
648
+ translations: {
649
+ de: 'Die Ersteinrichtung über die Oberfläche ist in dieser Umgebung nicht freigegeben.',
650
+ en: 'Initial admin setup over HTTP is not enabled in this environment.',
651
+ },
652
+ },
653
+
654
+ /**
655
+ * `POST /system-setup/init` requires the setup token (`systemSetup.httpInit.token`) in the `X-System-Setup-Token`
656
+ * header, and the request sent none or a different one (12.4.0+).
657
+ */
658
+ SYSTEM_SETUP_TOKEN_INVALID: {
659
+ code: 'LTNS_0054',
660
+ message: 'Missing or invalid system setup token',
661
+ translations: {
662
+ de: 'Der Einrichtungs-Token fehlt oder ist falsch.',
663
+ en: 'The system setup token is missing or invalid.',
664
+ },
665
+ },
666
+
640
667
  // =====================================================
641
668
  // AI Errors (LTNS_0600-LTNS_0699)
642
669
  // =====================================================
@@ -2,7 +2,9 @@
2
2
 
3
3
  **For initial admin creation on fresh deployments in projects using `@lenne.tech/nest-server`.**
4
4
 
5
- > **Note:** System setup is **enabled by default** when BetterAuth is active. No explicit configuration is needed for the REST endpoints to work.
5
+ > **Note:** System setup is **enabled by default** when BetterAuth is active. Locally (`local`, `ci`, `e2e`) the REST
6
+ > endpoints work without configuration. **In a deployed environment `POST /system-setup/init` is closed since 12.4.0**:
7
+ > provision the first admin via ENV (Step 1), or open the HTTP init on purpose (Step 1b).
6
8
 
7
9
  ---
8
10
 
@@ -10,8 +12,9 @@
10
12
 
11
13
  | Scenario | Action Needed |
12
14
  | ---------------------------------------------------- | ----------------------------------------------- |
13
- | Fresh deployment needs initial admin creation | None - endpoints are available by default |
14
- | Automated deployment (Docker/CI) needs auto-creation | Set ENV variables (Step 1) |
15
+ | Local development / e2e tests create the first admin | None - the HTTP init is open there by default |
16
+ | Deployed instance needs its first admin | Set ENV variables (Step 1) - recommended |
17
+ | Deployed instance must be set up through the UI | Open the HTTP init with a token (Step 1b) |
15
18
  | Want to disable system setup | Set `systemSetup: { enabled: false }` in config |
16
19
  | Custom setup logic (extra fields, notifications) | Step 2 (Custom Controller) |
17
20
 
@@ -35,12 +38,16 @@ https://github.com/lenneTech/nest-server/tree/develop/src/core/modules/system-se
35
38
  For automated deployments where no manual REST call is possible:
36
39
 
37
40
  ```bash
38
- # .env or Docker environment
39
- NSC__systemSetup__initialAdmin__email=admin@example.com
40
- NSC__systemSetup__initialAdmin__password=YourSecurePassword123!
41
- NSC__systemSetup__initialAdmin__name=Admin # optional
41
+ # .env or Docker environment — every segment in SCREAMING_SNAKE_CASE
42
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL=admin@example.com
43
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD=YourSecurePassword123!
44
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME=Admin # optional
42
45
  ```
43
46
 
47
+ **WHY SCREAMING_SNAKE_CASE?** The environment reader lowercases every segment it does not split at `_`. The camelCase
48
+ form `NSC__systemSetup__initialAdmin__email` (shown in this checklist until 12.4.0) is therefore never read, and no
49
+ admin is created. Since 12.4.0 the boot warns about such a variable.
50
+
44
51
  Or in `config.env.ts`:
45
52
 
46
53
  ```typescript
@@ -58,6 +65,26 @@ The admin is created automatically on server start when zero users exist.
58
65
 
59
66
  ---
60
67
 
68
+ ## Step 1b: Open the HTTP init on a deployed instance (Optional, 12.4.0+)
69
+
70
+ Only when the first admin must be created through the UI on a deployed stage:
71
+
72
+ ```bash
73
+ NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN=<secret of at least 16 characters> # recommended
74
+ # or, without a token (whoever reaches the API first becomes admin):
75
+ NSC__SYSTEM_SETUP__HTTP_INIT=true
76
+ ```
77
+
78
+ The frontend sends the token in the `X-System-Setup-Token` header; `GET /system-setup/status` tells it with
79
+ `httpInitEnabled` / `httpInitTokenRequired` whether to show the form and a token field.
80
+
81
+ **WHY closed by default?** The init is anonymous. On a deployment without `initialAdmin` it made whoever called it
82
+ first the administrator, until the first sign-up.
83
+
84
+ Remove the setting once the admin exists — the init is locked from then on anyway.
85
+
86
+ ---
87
+
61
88
  ## Step 2: Custom Controller (Optional)
62
89
 
63
90
  Only needed if you want to add extra validation, logging, or custom fields.
@@ -76,6 +103,9 @@ export class SystemSetupController extends CoreSystemSetupController {
76
103
  ```
77
104
 
78
105
  > **Note:** The SystemSetup module auto-registers its controller via CoreModule. To use a custom controller, you would need to disable auto-registration and register your own module.
106
+ >
107
+ > **An override of `createInitialAdmin()` must call `this.systemSetupService.assertHttpInitAllowed(token)` first**
108
+ > (the token from the `x-system-setup-token` header), or it opens the anonymous init in every environment.
79
109
 
80
110
  ---
81
111
 
@@ -84,7 +114,9 @@ export class SystemSetupController extends CoreSystemSetupController {
84
114
  - [ ] `npm run build` succeeds
85
115
  - [ ] `npm test` passes
86
116
  - [ ] `GET /system-setup/status` returns `{ needsSetup: true }` on empty database
87
- - [ ] `POST /system-setup/init` creates admin user with correct role
117
+ - [ ] Locally: `httpInitEnabled: true`, and `POST /system-setup/init` creates admin user with correct role
118
+ - [ ] Deployed stage without `httpInit`: `httpInitEnabled: false`, and the init answers 403 `LTNS_0053`
119
+ - [ ] Deployed stage with `initialAdmin`: the admin exists after the first start
88
120
  - [ ] `GET /system-setup/status` returns `{ needsSetup: false }` after init
89
121
  - [ ] `POST /system-setup/init` returns 403 when users already exist
90
122
 
@@ -92,14 +124,17 @@ export class SystemSetupController extends CoreSystemSetupController {
92
124
 
93
125
  ## Common Mistakes
94
126
 
95
- | Mistake | Symptom | Fix |
96
- | ------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
97
- | BetterAuth not enabled | 404 on endpoints or 403 on init | Ensure `betterAuth` is configured |
98
- | Calling init with existing users | 403 "System setup not available" | Init requires an empty `users` collection |
99
- | Re-running init after a successful setup | 403, even with an empty `users` | The claim marker in `system-setup-locks` outlives a SUCCESSFUL setup (it is released only on failure) — drop that collection too, or wait 5 min for it to go stale |
100
- | Password too short | 400 validation error | Password must be at least 8 characters |
101
- | Missing ENV password | Auto-creation silently skipped | Set both `email` and `password` ENV vars |
102
- | `systemSetup: { enabled: false }` in config | 404 on endpoints | Remove the explicit disable |
127
+ | Mistake | Symptom | Fix |
128
+ | --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
129
+ | BetterAuth not enabled | 404 on endpoints or 403 on init | Ensure `betterAuth` is configured |
130
+ | Calling init with existing users | 403 "System setup not available" | Init requires an empty `users` collection |
131
+ | Re-running init after a successful setup | 403, even with an empty `users` | The claim marker in `system-setup-locks` outlives a SUCCESSFUL setup (it is released only on failure) — drop that collection too, or wait 5 min for it to go stale |
132
+ | Password too short | 400 validation error | Password must be at least 8 characters |
133
+ | Missing ENV password | Auto-creation silently skipped | Set both `email` and `password` ENV vars |
134
+ | camelCase ENV names (`NSC__systemSetup__…`) | No admin, boot warning | Write every segment in SCREAMING_SNAKE_CASE: `NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL` |
135
+ | Setting up a deployed stage through the UI | 403 `LTNS_0053` | Set `initialAdmin` via ENV, or open the init with `NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN` |
136
+ | `httpInit: { token: process.env.X }`, X unset | 403 `LTNS_0053`, boot error | A missing, empty or short token keeps the init closed on purpose — set the variable |
137
+ | `systemSetup: { enabled: false }` in config | 404 on endpoints | Remove the explicit disable |
103
138
 
104
139
  ---
105
140
 
@@ -4,13 +4,20 @@ Initial admin user creation for fresh deployments of @lenne.tech/nest-server.
4
4
 
5
5
  ## TL;DR
6
6
 
7
- System setup is **enabled by default** when BetterAuth is active. No configuration needed.
7
+ System setup is **enabled by default** when BetterAuth is active. No configuration needed locally.
8
8
 
9
- For automated deployments (Docker, CI/CD), set initial admin credentials via ENV:
9
+ **In a deployed environment** (anything but `local`, `ci` and `e2e`) the anonymous `POST /system-setup/init` is
10
+ **closed** since 12.4.0. Provision the first admin via ENV instead — it is created on the next start:
10
11
 
11
12
  ```bash
12
- NSC__systemSetup__initialAdmin__email=admin@example.com
13
- NSC__systemSetup__initialAdmin__password=YourSecurePassword123!
13
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL=admin@example.com
14
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD=YourSecurePassword123!
15
+ ```
16
+
17
+ Or open the HTTP init on purpose, preferably with a setup token (see [HTTP init](#http-init-systemsetuphttpinit)):
18
+
19
+ ```bash
20
+ NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN=<secret of at least 16 characters> # sent as X-System-Setup-Token
14
21
  ```
15
22
 
16
23
  **Quick Links:** [Integration Checklist](./INTEGRATION-CHECKLIST.md) | [Endpoints](#endpoints) | [Configuration](#configuration) | [Security](#security)
@@ -22,6 +29,7 @@ NSC__systemSetup__initialAdmin__password=YourSecurePassword123!
22
29
  - [Purpose](#purpose)
23
30
  - [Endpoints](#endpoints)
24
31
  - [Configuration](#configuration)
32
+ - [HTTP init (`systemSetup.httpInit`)](#http-init-systemsetuphttpinit)
25
33
  - [Auto-Creation via Config/ENV](#auto-creation-via-configenv)
26
34
  - [Security](#security)
27
35
  - [Frontend Integration](#frontend-integration)
@@ -33,8 +41,8 @@ NSC__systemSetup__initialAdmin__password=YourSecurePassword123!
33
41
 
34
42
  When a system is freshly deployed (zero users in the database), there is no way to create an initial admin user - especially when BetterAuth's `disableSignUp` is enabled. This module provides:
35
43
 
36
- 1. **REST endpoints** - Manual admin creation via API call
37
- 2. **Auto-creation** - Automatic admin creation on server start via config/ENV
44
+ 1. **REST endpoints** - Manual admin creation via API call — open by default only in `local`, `ci` and `e2e`
45
+ 2. **Auto-creation** - Automatic admin creation on server start via config/ENV — the way for a deployment
38
46
 
39
47
  Once any user exists, the init endpoint is permanently locked (returns 403) and auto-creation is skipped.
40
48
 
@@ -56,13 +64,28 @@ Returns the current setup status.
56
64
  ```json
57
65
  {
58
66
  "needsSetup": true,
59
- "betterAuthEnabled": true
67
+ "betterAuthEnabled": true,
68
+ "httpInitEnabled": true,
69
+ "httpInitTokenRequired": false
60
70
  }
61
71
  ```
62
72
 
73
+ | Field | Meaning |
74
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
75
+ | `needsSetup` | No user exists yet |
76
+ | `betterAuthEnabled` | BetterAuth is active (required for the init) |
77
+ | `httpInitEnabled` | `POST /system-setup/init` is open in this environment (12.4.0+). Independent of `needsSetup` |
78
+ | `httpInitTokenRequired` | The init needs the `X-System-Setup-Token` header (12.4.0+). Only `true` together with `httpInitEnabled` |
79
+
80
+ A frontend offers the setup form only when `needsSetup && httpInitEnabled`, with a token field when
81
+ `httpInitTokenRequired`. A backend older than 12.4.0 sends neither field; its init is open as before.
82
+
63
83
  ### POST /system-setup/init
64
84
 
65
- Creates the initial admin user. Only works when zero users exist.
85
+ Creates the initial admin user. Only works when zero users exist, and only where `systemSetup.httpInit` opens it
86
+ (by default in `local`, `ci` and `e2e`).
87
+
88
+ **Request Header** (only when a token is configured): `X-System-Setup-Token: <setup token>`
66
89
 
67
90
  **Request Body:**
68
91
 
@@ -98,17 +121,21 @@ Creates the initial admin user. Only works when zero users exist.
98
121
  }
99
122
  ```
100
123
 
124
+ The refusals are checked in this order: `LTNS_0053` (HTTP init not enabled here), `LTNS_0054` (setup token missing
125
+ or wrong), then `LTNS_0050` and `LTNS_0052` as before. See [Error Codes](#error-codes).
126
+
101
127
  ---
102
128
 
103
129
  ## Configuration
104
130
 
105
131
  System setup is **enabled by default** when BetterAuth is active:
106
132
 
107
- | Config | Effect |
108
- | ---------------------------------------- | ----------------------------------- |
109
- | _(not set)_ | Enabled (when BetterAuth is active) |
110
- | `systemSetup: { enabled: false }` | Disabled explicitly |
111
- | `systemSetup: { initialAdmin: { ... } }` | Enabled with auto-creation |
133
+ | Config | Effect |
134
+ | ---------------------------------------- | --------------------------------------------------------------------- |
135
+ | _(not set)_ | Enabled (when BetterAuth is active); HTTP init open only locally |
136
+ | `systemSetup: { enabled: false }` | Disabled explicitly (no endpoints) |
137
+ | `systemSetup: { initialAdmin: { ... } }` | Enabled with auto-creation |
138
+ | `systemSetup: { httpInit: ... }` | Opens or closes `POST /system-setup/init` explicitly (12.4.0+, below) |
112
139
 
113
140
  ```typescript
114
141
  // config.env.ts
@@ -129,6 +156,54 @@ systemSetup: {
129
156
 
130
157
  ---
131
158
 
159
+ ## HTTP init (`systemSetup.httpInit`)
160
+
161
+ `POST /system-setup/init` is anonymous: on an API without users, whoever calls it becomes the administrator. Since
162
+ 12.4.0 it is therefore **fail-closed** outside the local environments.
163
+
164
+ | `httpInit` | `local` / `ci` / `e2e` | every other environment (`develop`, `test`, `production`, …) |
165
+ | --------------------------------------- | ---------------------- | ------------------------------------------------------------ |
166
+ | _(not set)_ | open | **closed** — 403 `LTNS_0053` |
167
+ | `true` | open | open, without a token |
168
+ | `{ token: '<at least 16 characters>' }` | open, token required | open, token required — 403 `LTNS_0054` without it |
169
+ | `false` / `{ enabled: false, token }` | closed | closed |
170
+
171
+ The environment is the configured `env` (else `NODE_ENV`), compared with the framework's one list of local
172
+ environments (`isLocalEnv()`). The deployed `develop` and `test` stages of a starter-based project, `production`,
173
+ `staging` and any unknown or missing name count as deployed.
174
+
175
+ ```typescript
176
+ // config.env.ts — open with a token (recommended when the setup must happen over the UI)
177
+ systemSetup: {
178
+ httpInit: { token: process.env.SETUP_TOKEN },
179
+ },
180
+ ```
181
+
182
+ ```bash
183
+ # the same via ENV — set ONE of the two
184
+ NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN=<secret of at least 16 characters>
185
+ NSC__SYSTEM_SETUP__HTTP_INIT=true # open WITHOUT a token: whoever is first becomes admin
186
+ ```
187
+
188
+ **The token** travels in the `X-System-Setup-Token` header, never in the body, and is compared in constant time.
189
+ Hand it to the person who sets the system up, the same way as a password.
190
+
191
+ **Misconfigurations close, they never open:**
192
+
193
+ - an object whose token is missing, empty, shorter than 16 characters or not a string keeps the init closed and logs
194
+ an error at boot. `{ token: process.env.SETUP_TOKEN }` with the variable unset must not become an open init.
195
+ - an empty or all-digit `NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN` is converted to a number by the environment reader, and
196
+ a long digit string loses digits on the way — such a value is refused. Use a token with letters.
197
+ - a value that is neither a boolean nor an object (the string `"true"` from `NEST_SERVER_CONFIG`) counts as not set.
198
+
199
+ **Boot report.** A deployed instance that starts with zero users, no usable `initialAdmin` and a closed HTTP init logs
200
+ an error naming both ways in. With the init open without a token it logs a warning. Local environments stay silent.
201
+
202
+ The Swagger document leaves `POST /system-setup/init` out while the init is closed, like every operation that would
203
+ refuse every caller.
204
+
205
+ ---
206
+
132
207
  ## Auto-Creation via Config/ENV
133
208
 
134
209
  For automated deployments where no manual REST call is possible (Docker, CI/CD, Kubernetes), configure initial admin credentials via environment variables:
@@ -136,11 +211,16 @@ For automated deployments where no manual REST call is possible (Docker, CI/CD,
136
211
  ### Via NSC Environment Variables
137
212
 
138
213
  ```bash
139
- NSC__systemSetup__initialAdmin__email=admin@example.com
140
- NSC__systemSetup__initialAdmin__password=YourSecurePassword123!
141
- NSC__systemSetup__initialAdmin__name=Admin # optional
214
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL=admin@example.com
215
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD=YourSecurePassword123!
216
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME=Admin # optional
142
217
  ```
143
218
 
219
+ Every segment in SCREAMING_SNAKE_CASE. The environment reader lowercases a segment it does not split at `_`, so the
220
+ camelCase form `NSC__systemSetup__initialAdmin__email` — which this README showed until 12.4.0 — lands under
221
+ `systemsetup.initialadmin` and is never read. Since 12.4.0 the boot warns about such a variable and names the correct
222
+ spelling.
223
+
144
224
  ### Via config.env.ts
145
225
 
146
226
  ```typescript
@@ -162,6 +242,7 @@ NEST_SERVER_CONFIG='{ "systemSetup": { "initialAdmin": { "email": "admin@example
162
242
  ### Behavior
163
243
 
164
244
  - The admin is created automatically during application bootstrap (`OnApplicationBootstrap`)
245
+ - Not affected by `systemSetup.httpInit`: the configured admin is created in every environment
165
246
  - Same zero-user guard applies: only works when no users exist
166
247
  - If users already exist, auto-creation is silently skipped (no error)
167
248
  - Race conditions between multiple instances are handled gracefully: the zero-user check alone is
@@ -187,14 +268,19 @@ NEST_SERVER_CONFIG='{ "systemSetup": { "initialAdmin": { "email": "admin@example
187
268
 
188
269
  ## Security
189
270
 
190
- 1. **Zero-user guard** - Init requires `countDocuments({}) === 0`. Necessary but not sufficient:
271
+ 1. **Fail-closed HTTP init (12.4.0)** - Outside `local`, `ci` and `e2e` the anonymous init answers 403
272
+ `LTNS_0053` unless `systemSetup.httpInit` opens it, preferably with a setup token. Before 12.4.0 a
273
+ deployment without `initialAdmin` could be taken over by whoever called the init first
274
+ 2. **Zero-user guard** - Init requires `countDocuments({}) === 0`. Necessary but not sufficient:
191
275
  the atomic claim below must also be winnable, so an empty `users` collection alone does not
192
276
  re-open setup
193
- 2. **Enabled by default** - Safe because endpoints are permanently locked once any user exists
194
- 3. **Race condition protection** - MongoDB unique email index prevents duplicates; auto-creation on
277
+ 3. **Rollback on failure (12.4.0)** - When a step after creating the user fails (account, sync, role),
278
+ the user is removed again, so `needsSetup` stays `true` and the setup can be retried. Before,
279
+ the deployment kept a user without the admin role and was locked for good
280
+ 4. **Race condition protection** - MongoDB unique email index prevents duplicates; auto-creation on
195
281
  bootstrap is additionally claimed atomically via the `system-setup-locks` marker
196
- 4. **Permanent lock** - Once any user exists, init returns 403
197
- 5. **BetterAuth required** - Returns 403 if BetterAuth is not enabled
282
+ 5. **Permanent lock** - Once any user exists, init returns 403
283
+ 6. **BetterAuth required** - Returns 403 if BetterAuth is not enabled
198
284
 
199
285
  ### How It Bypasses disableSignUp
200
286
 
@@ -207,15 +293,19 @@ The service uses BetterAuth's `$context.internalAdapter.createUser()` directly,
207
293
  Typical frontend flow:
208
294
 
209
295
  ```typescript
210
- // 1. Check if setup is needed
296
+ // 1. Check if setup is needed AND possible over HTTP
211
297
  const status = await fetch('/system-setup/status');
212
- const { needsSetup } = await status.json();
298
+ const { needsSetup, httpInitEnabled = true, httpInitTokenRequired = false } = await status.json();
299
+ // (the defaults keep a backend older than 12.4.0 working, where both fields are missing)
213
300
 
214
- if (needsSetup) {
215
- // 2. Show setup form and submit
301
+ if (needsSetup && httpInitEnabled) {
302
+ // 2. Show setup form (with a token field when httpInitTokenRequired) and submit
216
303
  const result = await fetch('/system-setup/init', {
217
304
  method: 'POST',
218
- headers: { 'Content-Type': 'application/json' },
305
+ headers: {
306
+ 'Content-Type': 'application/json',
307
+ ...(httpInitTokenRequired ? { 'X-System-Setup-Token': setupToken } : {}),
308
+ },
219
309
  body: JSON.stringify({
220
310
  email: 'admin@example.com',
221
311
  password: 'SecurePassword123!',
@@ -232,6 +322,29 @@ if (needsSetup) {
232
322
 
233
323
  ## Troubleshooting
234
324
 
325
+ ### Init returns 403 `LTNS_0053` "not enabled in this environment"
326
+
327
+ **Cause:** the environment is not `local`, `ci` or `e2e`, and `systemSetup.httpInit` does not open the init — the
328
+ default since 12.4.0.
329
+
330
+ **Solutions:**
331
+
332
+ 1. Set `NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL` / `__PASSWORD` and restart — the admin is created at boot
333
+ 2. Or open the init with a token: `NSC__SYSTEM_SETUP__HTTP_INIT__TOKEN=<secret>` (sent as `X-System-Setup-Token`)
334
+ 3. Check the boot log: an unusable token configuration is logged as an error naming what is wrong
335
+
336
+ ### Init returns 403 `LTNS_0054` "setup token is missing or invalid"
337
+
338
+ **Cause:** the init requires a token and the request sent none or a different one. Send the configured value in the
339
+ `X-System-Setup-Token` header. A frontend learns that a token is needed from `httpInitTokenRequired`.
340
+
341
+ ### `needsSetup` is `false`, but nobody can sign in as admin
342
+
343
+ **Cause (before 12.4.0):** a creation failed after the user was written (for example a failing account link), and
344
+ the user stayed behind without the admin role. Since 12.4.0 such a user is removed again. On an older version, or if
345
+ the server log says the partially created user "could not be removed", delete that user and its `account` rows and
346
+ retry.
347
+
235
348
  ### Init returns 403 "System setup not available"
236
349
 
237
350
  **Two different guards raise this, and the response cannot tell you which.**
@@ -290,11 +403,12 @@ DELETE FROM lt_locks WHERE id = 'system-setup:initial-admin';
290
403
 
291
404
  ### Auto-creation not working
292
405
 
293
- **Cause:** Missing or incomplete ENV variables.
406
+ **Cause:** Missing, incomplete or misspelled ENV variables.
294
407
 
295
408
  **Solutions:**
296
409
 
297
- 1. Verify both `email` and `password` are set
410
+ 1. Verify both `email` and `password` are set, spelled `NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL` /
411
+ `__PASSWORD` — a camelCase spelling (`NSC__systemSetup__…`) is not read; the boot warns about it
298
412
  2. Check server logs for `Auto-created initial admin on startup` or warning messages
299
413
  3. Ensure BetterAuth is fully initialized (check startup logs)
300
414
 
@@ -316,6 +430,8 @@ DELETE FROM lt_locks WHERE id = 'system-setup:initial-admin';
316
430
  | LTNS_0050 | `SYSTEM_SETUP_NOT_AVAILABLE` | Users already exist, OR the initial-admin setup is claimed |
317
431
  | LTNS_0051 | `SYSTEM_SETUP_DISABLED` | System setup is disabled in config |
318
432
  | LTNS_0052 | `SYSTEM_SETUP_BETTERAUTH_REQUIRED` | BetterAuth must be enabled |
433
+ | LTNS_0053 | `SYSTEM_SETUP_HTTP_INIT_DISABLED` | HTTP init not enabled in this environment (12.4.0+) |
434
+ | LTNS_0054 | `SYSTEM_SETUP_TOKEN_INVALID` | Setup token missing or wrong (12.4.0+) |
319
435
 
320
436
  ---
321
437
 
@@ -1,10 +1,15 @@
1
- import { Body, Controller, Get, Post } from '@nestjs/common';
2
- import { ApiBody, ApiOperation, ApiProperty, ApiResponse, ApiTags } from '@nestjs/swagger';
1
+ import { Body, Controller, Get, Headers, Post } from '@nestjs/common';
2
+ import { ApiBody, ApiHeader, ApiOperation, ApiProperty, ApiResponse, ApiTags } from '@nestjs/swagger';
3
3
  import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
4
4
 
5
5
  import { Roles } from '../../common/decorators/roles.decorator.js';
6
6
  import { RoleEnum } from '../../common/enums/role.enum.js';
7
- import { CoreSystemSetupService, SystemSetupInitResult, SystemSetupStatus } from './core-system-setup.service.js';
7
+ import {
8
+ CoreSystemSetupService,
9
+ type SystemSetupInitResult,
10
+ type SystemSetupStatus,
11
+ } from './core-system-setup.service.js';
12
+ import { SYSTEM_SETUP_TOKEN_HEADER } from './system-setup-http-init.helper.js';
8
13
 
9
14
  /**
10
15
  * DTO for system setup init request
@@ -31,8 +36,12 @@ export class SystemSetupInitDto {
31
36
  * CoreSystemSetupController provides REST endpoints for initial system setup.
32
37
  *
33
38
  * These endpoints are public (S_EVERYONE) to allow first-time admin creation
34
- * on a fresh system with zero users. The service enforces security by checking
35
- * that no users exist before allowing creation.
39
+ * on a fresh system with zero users. The service enforces security: the init is
40
+ * refused where `systemSetup.httpInit` does not open it (by default every
41
+ * environment except `local`, `ci` and `e2e`), and once any user exists.
42
+ *
43
+ * A subclass that overrides `createInitialAdmin()` must call
44
+ * `systemSetupService.assertHttpInitAllowed(token)` itself.
36
45
  */
37
46
  @ApiTags('System Setup')
38
47
  @Controller('system-setup')
@@ -55,18 +64,31 @@ export class CoreSystemSetupController {
55
64
  * Create the initial admin user (only works when zero users exist)
56
65
  */
57
66
  @ApiBody({ type: SystemSetupInitDto })
67
+ @ApiHeader({
68
+ description: 'The setup token, required when systemSetup.httpInit configures one (see GET /system-setup/status)',
69
+ name: SYSTEM_SETUP_TOKEN_HEADER,
70
+ required: false,
71
+ })
58
72
  @ApiOperation({
59
- description: 'Creates the initial admin user. Only works when zero users exist in the database.',
73
+ description:
74
+ 'Creates the initial admin user. Only works when zero users exist in the database, and only where ' +
75
+ 'systemSetup.httpInit opens it (by default in the local, ci and e2e environments).',
60
76
  summary: 'Create initial admin',
61
77
  })
62
78
  @ApiResponse({ description: 'Initial admin created', status: 201 })
63
79
  @ApiResponse({
64
- description: 'System setup not available - users already exist, or the initial-admin setup is claimed',
80
+ description:
81
+ 'LTNS_0053: HTTP init not enabled in this environment; LTNS_0054: setup token missing or invalid; ' +
82
+ 'LTNS_0050: users already exist, or the initial-admin setup is claimed',
65
83
  status: 403,
66
84
  })
67
85
  @Post('init')
68
86
  @Roles(RoleEnum.S_EVERYONE)
69
- async createInitialAdmin(@Body() input: SystemSetupInitDto): Promise<SystemSetupInitResult> {
87
+ async createInitialAdmin(
88
+ @Body() input: SystemSetupInitDto,
89
+ @Headers(SYSTEM_SETUP_TOKEN_HEADER) token?: string,
90
+ ): Promise<SystemSetupInitResult> {
91
+ this.systemSetupService.assertHttpInitAllowed(token);
70
92
  return this.systemSetupService.createInitialAdmin(input);
71
93
  }
72
94
  }