@lenne.tech/nest-server 11.31.2 → 11.31.3
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.
- package/.claude/rules/architecture.md +2 -0
- package/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +28 -1
- package/FRAMEWORK-API.md +4 -1
- package/dist/config.env.d.ts +4 -0
- package/dist/config.env.js +32 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +1 -0
- package/dist/core/common/helpers/logging.helper.js +12 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/meta.helper.d.ts +1 -0
- package/dist/core/common/helpers/meta.helper.js +32 -1
- package/dist/core/common/helpers/meta.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +9 -0
- package/dist/core/common/services/email.service.d.ts +3 -1
- package/dist/core/common/services/email.service.js +33 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/template.service.js +9 -4
- package/dist/core/common/services/template.service.js.map +1 -1
- package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
- package/dist/core/modules/hub/core-hub-html.service.js +123 -0
- package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
- package/dist/core/modules/hub/core-hub.controller.js +398 -0
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
- package/dist/core/modules/hub/core-hub.module.js +109 -0
- package/dist/core/modules/hub/core-hub.module.js.map +1 -0
- package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
- package/dist/core/modules/hub/core-hub.service.js +153 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
- package/dist/core/modules/hub/hub-action-messages.js +21 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
- package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
- package/dist/core/modules/hub/hub-config.helper.js +164 -0
- package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-nav.d.ts +12 -0
- package/dist/core/modules/hub/hub-nav.js +43 -0
- package/dist/core/modules/hub/hub-nav.js.map +1 -0
- package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
- package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
- package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
- package/dist/core/modules/hub/hub.constants.d.ts +18 -0
- package/dist/core/modules/hub/hub.constants.js +22 -0
- package/dist/core/modules/hub/hub.constants.js.map +1 -0
- package/dist/core/modules/hub/index.d.ts +25 -0
- package/dist/core/modules/hub/index.js +42 -0
- package/dist/core/modules/hub/index.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
- package/dist/core.module.js +42 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/package.json +5 -4
- package/src/config.env.ts +75 -2
- package/src/core/common/helpers/logging.helper.spec.ts +61 -0
- package/src/core/common/helpers/logging.helper.ts +48 -0
- package/src/core/common/helpers/meta.helper.ts +46 -1
- package/src/core/common/interfaces/server-options.interface.ts +46 -0
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
- package/src/core/modules/hub/README.md +159 -0
- package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
- package/src/core/modules/hub/core-hub-html.service.ts +135 -0
- package/src/core/modules/hub/core-hub.controller.ts +286 -0
- package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
- package/src/core/modules/hub/core-hub.module.ts +159 -0
- package/src/core/modules/hub/core-hub.service.ts +169 -0
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
- package/src/core/modules/hub/hub-action-messages.ts +47 -0
- package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
- package/src/core/modules/hub/hub-config.helper.ts +233 -0
- package/src/core/modules/hub/hub-nav.ts +66 -0
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
- package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
- package/src/core/modules/hub/hub.constants.ts +84 -0
- package/src/core/modules/hub/index.ts +25 -0
- package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
- package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
- package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
- package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
- package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
- package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
- package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
- package/src/core.module.ts +61 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Hub Integration Checklist
|
|
2
|
+
|
|
3
|
+
The Hub is **config-only** — no files need to be created in the consuming project. It is auto-registered
|
|
4
|
+
by `CoreModule.forRoot()` when `hub` is present in the config.
|
|
5
|
+
|
|
6
|
+
## Reference Implementation
|
|
7
|
+
|
|
8
|
+
- Local: `node_modules/@lenne.tech/nest-server/src/core/modules/hub/`
|
|
9
|
+
- The framework's own e2e config (`src/config.env.ts`) enables it in `local`/`development`/`e2e`/`ci`.
|
|
10
|
+
|
|
11
|
+
## Quick Setup
|
|
12
|
+
|
|
13
|
+
### 1. Enable per environment
|
|
14
|
+
|
|
15
|
+
**Edit:** `src/config.env.ts`
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// development / local
|
|
19
|
+
hub: {
|
|
20
|
+
collectors: { queries: true }, // opt-in query profiler
|
|
21
|
+
mailbox: { mode: 'capture' }, // capture outgoing mail locally (Mailpit replacement)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// production — usually omit `hub` entirely, or (if you want it, still ADMIN-gated):
|
|
25
|
+
// hub: { collectors: { queries: false }, mailbox: false },
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. Sign in as a user with `RoleEnum.ADMIN` and open `/hub`.
|
|
29
|
+
|
|
30
|
+
## Optional Enhancements
|
|
31
|
+
|
|
32
|
+
| Want | Do |
|
|
33
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
34
|
+
| **Cron panel populated** | Import `ScheduleModule.forRoot()` in your ServerModule (usually already present). |
|
|
35
|
+
| **Query profiler** | Set `hub.collectors.queries: true` (opts the driver into `monitorCommands`). |
|
|
36
|
+
| **Migrations run/rollback** | Set `hub.migrations.dir` to your migrations directory (default `./migrations`). In compiled deployments point it at the built JS. Use `lockCollectionName` for cluster safety. |
|
|
37
|
+
| **Custom look / behavior** | Pass `overrides.hub.{controller,actionsController,service,htmlService,actionsService}` to `CoreModule.forRoot()`. |
|
|
38
|
+
|
|
39
|
+
## Verification Checklist
|
|
40
|
+
|
|
41
|
+
- [ ] `pnpm run build` succeeds
|
|
42
|
+
- [ ] Sign in as ADMIN → `GET /hub` returns the dashboard HTML
|
|
43
|
+
- [ ] A non-admin user gets `403`; an anonymous request gets `401`
|
|
44
|
+
- [ ] `GET /hub/dashboard.json` returns build/memory/features
|
|
45
|
+
- [ ] `GET /hub/config.json` shows `***` for secrets (never the real values)
|
|
46
|
+
- [ ] Mutating actions without `X-Hub-Request: 1` return `403`
|
|
47
|
+
|
|
48
|
+
## Common Mistakes
|
|
49
|
+
|
|
50
|
+
| Mistake | Symptom | Fix |
|
|
51
|
+
| ------------------------------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `hub` not set in the target environment | `/hub` → 404 | Add `hub: true` (or an options object) to that env's config block |
|
|
53
|
+
| `mailbox.mode: 'capture'` in production | Startup error | Use `mode: 'copy'` or disable the mailbox in production — capture suppresses all mail |
|
|
54
|
+
| Query panel empty | queries collector off (default) | `hub.collectors.queries: true` |
|
|
55
|
+
| Cron panel empty | `ScheduleModule.forRoot()` not imported | Import it in your ServerModule |
|
|
56
|
+
| Hub path collides with a project route | 404 / wrong page | Set a distinct `hub.path` (e.g. `admin/hub`) |
|
|
57
|
+
| No roles guard registered (auth system disabled) | Every `/hub` sidecar/action returns 200 for anonymous requests | The Hub relies on the framework's `RolesGuard`/`BetterAuthRolesGuard` (registered by the auth/BetterAuth module) to enforce `@Roles(RoleEnum.ADMIN)`. If you disable BetterAuth AND legacy auth, no guard runs and the ADMIN gate is inert. Keep an auth module enabled, or do not expose the Hub. |
|
|
58
|
+
|
|
59
|
+
> **Auth dependency (why):** the Hub does not register its own guard — it assigns `@Roles(hub.roles)`
|
|
60
|
+
> (default `ADMIN`) as metadata and depends on the app-wide `RolesGuard` / `BetterAuthRolesGuard` to
|
|
61
|
+
> read it. That guard ships with the auth/BetterAuth modules. In a normal project one of them is
|
|
62
|
+
> active, so the gate works out of the box; but an app that runs with _no_ auth system has no guard
|
|
63
|
+
> to enforce the roles, and the sidecars/actions would be reachable unauthenticated. Either keep an
|
|
64
|
+
> auth module enabled or leave the Hub disabled in such a setup.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Hub — Admin Area (Operator Cockpit)
|
|
2
|
+
|
|
3
|
+
A build-free, ADMIN-gated dashboard of runtime information and admin tools, served directly by the
|
|
4
|
+
framework. Inspired by the sister project [nest-base](https://github.com/lenneTech/nest-base)'s Hub,
|
|
5
|
+
but adapted to this stack (NestJS + GraphQL + Mongoose) and shipped as **dependency-free server-side
|
|
6
|
+
HTML + a vanilla-JS SPA** (no React, no build step) so it works identically in npm- and vendor-mode.
|
|
7
|
+
|
|
8
|
+
## Enable it
|
|
9
|
+
|
|
10
|
+
The Hub is **never enabled implicitly** — switch it on per environment:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// config.env.ts
|
|
14
|
+
{
|
|
15
|
+
hub: true, // enabled at /hub, admin-only, default collectors
|
|
16
|
+
// or, with options:
|
|
17
|
+
hub: {
|
|
18
|
+
path: 'hub', // default 'hub'
|
|
19
|
+
collectors: { queries: true }, // query profiler is opt-in (enables driver command monitoring)
|
|
20
|
+
mailbox: { mode: 'capture' }, // built-in Mailpit-style mail capture (dev/test)
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Do NOT set `hub` in the `production` block unless you intend the cockpit to be reachable there
|
|
26
|
+
(it stays ADMIN-gated). See `IHubConfig` in `interfaces/hub-config.interface.ts` for every option.
|
|
27
|
+
|
|
28
|
+
## Panels
|
|
29
|
+
|
|
30
|
+
| Panel | Route | Source |
|
|
31
|
+
| -------------------- | --------------------- | --------------------------------------------------------------- |
|
|
32
|
+
| Dashboard | `/hub` | health, build info, memory, feature matrix, links |
|
|
33
|
+
| Diagnostics | `/hub/diagnostics` | heap/rss, node/platform, collector buffer levels |
|
|
34
|
+
| Logs | `/hub/logs` | in-memory log ring buffer (redacted) |
|
|
35
|
+
| Request Traces | `/hub/traces` | HTTP timing middleware |
|
|
36
|
+
| Query Performance | `/hub/queries` | MongoDB driver command monitoring (opt-in) |
|
|
37
|
+
| Cron Jobs | `/hub/cron` | `@nestjs/schedule` `SchedulerRegistry` (optional) |
|
|
38
|
+
| Database | `/hub/db` | dbStats / per-collection collStats |
|
|
39
|
+
| Models / ERD | `/hub/models` | Mongoose schemas → Mermaid ER diagram |
|
|
40
|
+
| Migrations | `/hub/migrations` | `MigrationRunner` status + run/rollback |
|
|
41
|
+
| Files | `/hub/files` | GridFS listing + delete |
|
|
42
|
+
| Config | `/hub/config` | full config, secrets masked |
|
|
43
|
+
| Auth Migration | `/hub/auth-migration` | Legacy → IAM progress (BetterAuth, optional) |
|
|
44
|
+
| Routes / Permissions | `/hub/routes` | route + role + `@Restricted` map (Permissions module, optional) |
|
|
45
|
+
| Error Codes | `/hub/error-codes` | de/en catalog (ErrorCode module, optional) |
|
|
46
|
+
| Email Preview | `/hub/emails` | EJS templates rendered with sample data |
|
|
47
|
+
| Mailbox | `/hub/mailbox` | captured outgoing mail (Mailpit replacement) |
|
|
48
|
+
| AI | `/hub/ai` | AI usage summary (AI module, optional) |
|
|
49
|
+
|
|
50
|
+
Each panel has a `*.json` sidecar (the stable data contract) that the client polls. Optional sources
|
|
51
|
+
degrade to an "unavailable" state instead of erroring.
|
|
52
|
+
|
|
53
|
+
## Actions (mutating)
|
|
54
|
+
|
|
55
|
+
Enabled by default (`actions: true`). Every mutating request requires the `X-Hub-Request: 1` header
|
|
56
|
+
(CSRF defense) and destructive ones a server-validated `confirm` keyword:
|
|
57
|
+
|
|
58
|
+
| Action | Endpoint | Confirm |
|
|
59
|
+
| ----------------------- | ------------------------------------------ | ------------ |
|
|
60
|
+
| Run pending migrations | `POST /hub/actions/migrations/run` | `RUN` |
|
|
61
|
+
| Rollback last migration | `POST /hub/actions/migrations/down` | `DOWN` |
|
|
62
|
+
| Delete GridFS file | `DELETE /hub/actions/files/:id` | the filename |
|
|
63
|
+
| Cron start/stop/trigger | `POST /hub/actions/cron/:name/:action` | the job name |
|
|
64
|
+
| Clear collector buffer | `POST /hub/actions/collectors/:name/clear` | `CLEAR` |
|
|
65
|
+
| Send test mail | `POST /hub/actions/email/test` | — |
|
|
66
|
+
|
|
67
|
+
Every action writes an audit line: `[HUB-ACTION] <action> by user <id>`.
|
|
68
|
+
|
|
69
|
+
## Security
|
|
70
|
+
|
|
71
|
+
- **Auth**: the DATA sidecars (`*.json`) and all actions are `@Roles(RoleEnum.ADMIN)` (configurable
|
|
72
|
+
via `hub.roles`; `false` = public — dangerous). The **shell** (page routes) is public chrome only —
|
|
73
|
+
it shows a **login form** when the data is 401, so the Hub is self-sufficient: an admin can sign in
|
|
74
|
+
directly at the API (email/password → `loginEndpoint`, default `/iam/sign-in/email` → session cookie)
|
|
75
|
+
without the frontend, with a token-paste fallback for cookie-less setups. In fullstack, a
|
|
76
|
+
cross-subdomain session cookie from the app login already authenticates `/hub` — no separate login.
|
|
77
|
+
A **"Sign out"** button in the topbar POSTs to `hub.logoutEndpoint` (default `/iam/sign-out`),
|
|
78
|
+
clears the session cookie + any pasted token, and returns to the login gate. The logout endpoint is
|
|
79
|
+
delivered only in the ADMIN-gated `session.json` payload, never in the public shell.
|
|
80
|
+
- **Public-shell trade-off (by design):** because the shell must render before authentication, its
|
|
81
|
+
HTML source reveals the panel structure, the environment name (shown on the login card) and any
|
|
82
|
+
configured external links (`hub.links.*`) to an unauthenticated request. This is intentional — the
|
|
83
|
+
panel structure is already discoverable via the public `hub.js` — and carries no data: every
|
|
84
|
+
`*.json` sidecar and every action stays ADMIN-gated. Do not place secrets in `hub.links.*` (e.g.
|
|
85
|
+
an internal Mailpit URL) if the shell's origin is reachable by untrusted clients.
|
|
86
|
+
- **Action errors**: mutating actions return plain admin-facing messages (not `ErrorCode` catalog
|
|
87
|
+
entries) by design — the `ErrorCode` i18n module is optional and the Hub cannot hard-depend on it.
|
|
88
|
+
The messages are centralized in `hub-action-messages.ts` (`HubActionMessage`) so the wording stays
|
|
89
|
+
consistent and reviewable in one place.
|
|
90
|
+
- **CSP**: strict per-request nonce; no `unsafe-inline`; `X-Frame-Options: DENY`; `no-store`.
|
|
91
|
+
- **Secrets**: the config viewer deep-clones and masks by key pattern + `security.secretFields`.
|
|
92
|
+
- **Interceptor safety**: sidecars return pre-serialized JSON strings, so the global response
|
|
93
|
+
interceptors never walk/mutate live config.
|
|
94
|
+
- **Mailbox guard**: `mode: 'capture'` throws at startup in any **reachable** environment (anything
|
|
95
|
+
whose `env` is not `local`/`development`/`test`/`ci`/`e2e`) — it suppresses outgoing mail.
|
|
96
|
+
- **Public-access guard**: `roles: false` (no auth check — public config viewer, logs and destructive
|
|
97
|
+
actions) throws at startup in any **reachable** environment unless `hub.allowPublicAccessInProduction:
|
|
98
|
+
true` is set explicitly. This closes the single most dangerous Hub misconfiguration (a `roles: false`
|
|
99
|
+
copied from a local config into a reachable environment). The reachable check is fail-safe: it treats
|
|
100
|
+
every env name except the known local/test set as reachable, so a custom name (`prod`, `preprod`,
|
|
101
|
+
`staging-2`, …) cannot bypass it. Only acknowledge public access behind a fully-controlled network
|
|
102
|
+
boundary (VPN / IP allow-list / authenticating reverse proxy).
|
|
103
|
+
- **No-guard warning**: the Hub registers no guard of its own — its ADMIN gate is enforced by the
|
|
104
|
+
app-wide roles guard from BetterAuth (IAM) or the legacy Auth module. If the Hub is enabled and
|
|
105
|
+
gated but neither is active (and you have not registered your own `APP_GUARD`), `CoreModule` logs a
|
|
106
|
+
loud startup warning, because nothing would then enforce the gate.
|
|
107
|
+
- **CSRF**: mutating actions require the `X-Hub-Request` custom header, which forces a CORS preflight
|
|
108
|
+
that a restrictive allowlist rejects for foreign origins — **that header** (not the type-to-confirm
|
|
109
|
+
keyword, which is public UX safety, not a token) is the CSRF barrier. It holds with the default
|
|
110
|
+
`SameSite=Lax` session cookie or Bearer-token auth. Do NOT combine `cors.allowAll: true` with a
|
|
111
|
+
`SameSite=None` cookie (cross-subdomain fullstack): that lets any origin pass the preflight — pin
|
|
112
|
+
`cors` to your `appUrl` instead.
|
|
113
|
+
- **Redaction is best-effort (ADMIN-only surfaces)**: the config viewer masks by key pattern +
|
|
114
|
+
`security.secretFields` (a secret under an unusual key can slip); the logs/queries collectors and
|
|
115
|
+
the copy-mode mailbox redact patterned secrets (JWT / Bearer / `key=value` / cookie / reset-link
|
|
116
|
+
path tokens) but cannot catch an arbitrary value logged without a recognizable shape. All are
|
|
117
|
+
ADMIN-gated — treat Hub access as equivalent to config/log read access.
|
|
118
|
+
|
|
119
|
+
### Routes / Permissions panel vs. the standalone `/permissions` endpoint
|
|
120
|
+
|
|
121
|
+
The **Routes / Permissions** panel renders the same data as the standalone permissions module — the
|
|
122
|
+
full security map (every route + its required roles + `@Restricted` field rules) — but _inside_ the
|
|
123
|
+
Hub, so it inherits the Hub's ADMIN gate, per-environment opt-in and strict CSP. It reuses the
|
|
124
|
+
permissions **scanner** (`CorePermissionsService`), so it needs the `permissions` module enabled
|
|
125
|
+
(`config.permissions`); otherwise the panel degrades to an "unavailable" state.
|
|
126
|
+
|
|
127
|
+
The two surfaces are **independent** and answer the common "how do I reach it?" questions:
|
|
128
|
+
|
|
129
|
+
| Situation | How to reach the report |
|
|
130
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| Hub **on**, `permissions` **on** | Hub → **Routes / Permissions** panel (ADMIN, login-gated by the Hub). The standalone `/permissions` also works per its own `role` (default ADMIN). |
|
|
132
|
+
| Hub **off**, `permissions` **on** (ADMIN) | The standalone `/permissions` is ADMIN-gated but has **no login page** — reach it with an ADMIN **session cookie** (be logged in via the app) or a **bearer token** (`Authorization: Bearer <jwt>` from `POST /iam/sign-in/email`), exactly like any other ADMIN endpoint. |
|
|
133
|
+
| Hub **off**, want frictionless **local** access | Set `permissions: { role: false }` — public, no auth. Legitimate **only** on a local, non-network-reachable machine, as a conscious opt-in. Never ship it to a reachable environment: the report is a reconnaissance goldmine. |
|
|
134
|
+
| `permissions` **off** | No report anywhere: `/permissions` is not registered (404) and the Hub panel shows "unavailable". |
|
|
135
|
+
|
|
136
|
+
**Security note:** the permissions report exposes your entire authorization model. Keep it ADMIN
|
|
137
|
+
(the default) everywhere it could be reachable; use `role: false` only behind a network boundary you
|
|
138
|
+
fully control. In production the framework's own config does not register the permissions module at all.
|
|
139
|
+
|
|
140
|
+
## Collectors
|
|
141
|
+
|
|
142
|
+
Three in-memory ring buffers (fixed capacity, no timers, per-app-instance — parallel-test-safe):
|
|
143
|
+
|
|
144
|
+
- **Logs** — installs a chaining `Logger.overrideLogger()` delegate (no main.ts change), restored on shutdown.
|
|
145
|
+
- **Traces** — an Express middleware registered only when enabled (zero cost otherwise).
|
|
146
|
+
- **Queries** — MongoDB driver command monitoring; records value-free query SHAPES (N+1 templates), never values.
|
|
147
|
+
Enabling it opts the driver into `monitorCommands` from `core.module.ts`.
|
|
148
|
+
|
|
149
|
+
## Overrides
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
CoreModule.forRoot(envConfig, {
|
|
153
|
+
hub: { service: MyHubService, htmlService: MyHubHtmlService },
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Fields: `controller`, `actionsController`, `service`, `htmlService`, `actionsService`.
|
|
158
|
+
|
|
159
|
+
See [INTEGRATION-CHECKLIST.md](./INTEGRATION-CHECKLIST.md) for setup in a consumer project.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
ForbiddenException,
|
|
7
|
+
Headers,
|
|
8
|
+
Param,
|
|
9
|
+
Post,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
|
|
12
|
+
import { HubActionMessage } from './hub-action-messages';
|
|
13
|
+
import { CoreHubActionsService } from './services/core-hub-actions.service';
|
|
14
|
+
|
|
15
|
+
/** Body shape shared by the confirm-guarded actions. */
|
|
16
|
+
interface ConfirmBody {
|
|
17
|
+
confirm?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The Hub's mutating actions (migrations, file delete, cron control, buffer clears, test mail).
|
|
22
|
+
*
|
|
23
|
+
* Registered as a SEPARATE controller only when `hub.actions !== false`, so disabling actions simply
|
|
24
|
+
* removes the routes (404). Path + roles metadata are assigned at runtime in `CoreHubModule.forRoot()`
|
|
25
|
+
* exactly like the read controller.
|
|
26
|
+
*
|
|
27
|
+
* Every mutating request must carry the `X-Hub-Request: 1` header (CSRF defense in depth: a custom
|
|
28
|
+
* header makes the request non-simple, so a cross-origin attempt triggers a CORS preflight the
|
|
29
|
+
* framework will not approve). Destructive actions additionally require a server-validated `confirm`
|
|
30
|
+
* keyword that the UI makes the operator type.
|
|
31
|
+
*
|
|
32
|
+
* Overridable via `overrides.hub.actionsController`.
|
|
33
|
+
*/
|
|
34
|
+
@Controller()
|
|
35
|
+
export class CoreHubActionsController {
|
|
36
|
+
constructor(protected readonly actions: CoreHubActionsService) {}
|
|
37
|
+
|
|
38
|
+
@Post('actions/collectors/:name/clear')
|
|
39
|
+
async clearCollector(
|
|
40
|
+
@Param('name') name: string,
|
|
41
|
+
@Body() body: ConfirmBody,
|
|
42
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
43
|
+
): Promise<unknown> {
|
|
44
|
+
this.requireHubRequest(hubHeader);
|
|
45
|
+
this.requireConfirm(body, 'CLEAR');
|
|
46
|
+
if (!['logs', 'mailbox', 'queries', 'traces'].includes(name)) {
|
|
47
|
+
throw new BadRequestException(HubActionMessage.unknownCollector);
|
|
48
|
+
}
|
|
49
|
+
return this.wrap(() =>
|
|
50
|
+
Promise.resolve(this.actions.clearBuffer(name as 'logs' | 'mailbox' | 'queries' | 'traces')),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Post('actions/cron/:name/:action')
|
|
55
|
+
async cron(
|
|
56
|
+
@Param('name') name: string,
|
|
57
|
+
@Param('action') action: string,
|
|
58
|
+
@Body() body: ConfirmBody,
|
|
59
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
60
|
+
): Promise<unknown> {
|
|
61
|
+
this.requireHubRequest(hubHeader);
|
|
62
|
+
if (!['start', 'stop', 'trigger'].includes(action)) {
|
|
63
|
+
throw new BadRequestException(HubActionMessage.unknownCronAction);
|
|
64
|
+
}
|
|
65
|
+
this.requireConfirm(body, name);
|
|
66
|
+
return this.wrap(() => Promise.resolve(this.actions.controlCron(name, action as 'start' | 'stop' | 'trigger')));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Delete('actions/files/:id')
|
|
70
|
+
async deleteFile(
|
|
71
|
+
@Param('id') id: string,
|
|
72
|
+
@Body() body: ConfirmBody,
|
|
73
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
74
|
+
): Promise<unknown> {
|
|
75
|
+
this.requireHubRequest(hubHeader);
|
|
76
|
+
if (!body?.confirm) {
|
|
77
|
+
throw new BadRequestException(HubActionMessage.confirmationFilenameRequired);
|
|
78
|
+
}
|
|
79
|
+
return this.wrap(() => this.actions.deleteFile(id, body.confirm as string));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Post('actions/email/test')
|
|
83
|
+
async emailTest(
|
|
84
|
+
@Body() body: { locale?: string; template?: string; to?: string },
|
|
85
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
86
|
+
): Promise<unknown> {
|
|
87
|
+
this.requireHubRequest(hubHeader);
|
|
88
|
+
if (!body?.to) {
|
|
89
|
+
throw new BadRequestException(HubActionMessage.recipientRequired);
|
|
90
|
+
}
|
|
91
|
+
return this.wrap(() => this.actions.sendTestEmail(body.to as string, body.template, body.locale));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Post('actions/migrations/down')
|
|
95
|
+
async migrationsDown(
|
|
96
|
+
@Body() body: ConfirmBody,
|
|
97
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
98
|
+
): Promise<unknown> {
|
|
99
|
+
this.requireHubRequest(hubHeader);
|
|
100
|
+
this.requireConfirm(body, 'DOWN');
|
|
101
|
+
return this.wrap(() => this.actions.rollbackMigration());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Post('actions/migrations/run')
|
|
105
|
+
async migrationsRun(
|
|
106
|
+
@Body() body: ConfirmBody,
|
|
107
|
+
@Headers('x-hub-request') hubHeader: string | undefined,
|
|
108
|
+
): Promise<unknown> {
|
|
109
|
+
this.requireHubRequest(hubHeader);
|
|
110
|
+
this.requireConfirm(body, 'RUN');
|
|
111
|
+
return this.wrap(() => this.actions.runMigrations());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Validate the server-side confirmation keyword. */
|
|
115
|
+
protected requireConfirm(body: ConfirmBody, expected: string): void {
|
|
116
|
+
if (body?.confirm !== expected) {
|
|
117
|
+
throw new BadRequestException(HubActionMessage.confirmationKeywordMismatch(expected));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** CSRF defense: mutating requests must carry the X-Hub-Request header. */
|
|
122
|
+
protected requireHubRequest(header: string | undefined): void {
|
|
123
|
+
if (!header) {
|
|
124
|
+
throw new ForbiddenException(HubActionMessage.missingHubRequestHeader);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Run an action, mapping domain errors to 400 (admin-facing plain message). */
|
|
129
|
+
protected async wrap(fn: () => Promise<unknown>): Promise<unknown> {
|
|
130
|
+
try {
|
|
131
|
+
const result = await fn();
|
|
132
|
+
return { ok: true, timestamp: new Date().toISOString(), ...(result as object) };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new BadRequestException(error instanceof Error ? error.message : HubActionMessage.actionFailed);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { HUB_CONFIG } from './hub.constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-process cache-bust token for the client bundle (`hub.js?v=<token>`). It changes on every server
|
|
7
|
+
* start, so the browser refetches the script after a restart/redeploy (a dev code change is never
|
|
8
|
+
* masked by the immutable cache), while a single boot keeps the long-lived cache. The app version is
|
|
9
|
+
* deliberately NOT part of this public URL — it would leak the version to unauthenticated requests
|
|
10
|
+
* (the version is shown only in the ADMIN-gated, client-built topbar from session.json).
|
|
11
|
+
*/
|
|
12
|
+
const HUB_ASSET_TOKEN = Date.now().toString(36);
|
|
13
|
+
import { getHubClientJs } from './helpers/hub-client-js.helper';
|
|
14
|
+
import { escapeHtml, escapeJsString, HUB_NONCE_PLACEHOLDER } from './helpers/hub-shell.helper';
|
|
15
|
+
import { ResolvedHubConfig } from './interfaces/hub-config.interface';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds the Hub's single, self-contained HTML shell. Every page route serves the same shell (the
|
|
19
|
+
* client router picks the active panel from the URL), so this is stateless apart from the injected
|
|
20
|
+
* config. All server-provided values are escaped; the auth token (when present) is embedded as a JS
|
|
21
|
+
* string so sidecar fetches carry it in cookie-less setups.
|
|
22
|
+
*
|
|
23
|
+
* Overridable: a project may replace it via `overrides.hub.htmlService` to re-skin the cockpit.
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class CoreHubHtmlService {
|
|
27
|
+
constructor(@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig) {}
|
|
28
|
+
|
|
29
|
+
/** The shared client runtime script — served verbatim from `GET /{hub}/hub.js`. */
|
|
30
|
+
buildClientScript(): string {
|
|
31
|
+
return getHubClientJs();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the full shell HTML with `__CSP_NONCE__` placeholders. The controller replaces the
|
|
36
|
+
* placeholders per request via `injectNonce()`.
|
|
37
|
+
*
|
|
38
|
+
* @param authHeader - the incoming `Authorization` header, embedded so client fetches can replay it
|
|
39
|
+
*/
|
|
40
|
+
buildShell(authHeader?: string): string {
|
|
41
|
+
const base = '/' + this.config.path;
|
|
42
|
+
|
|
43
|
+
const bootstrap = [
|
|
44
|
+
`window.__HUB_BASE__='${escapeJsString(base)}';`,
|
|
45
|
+
`window.__HUB_POLL_MS__=${Number(this.config.pollIntervalMs) || 5000};`,
|
|
46
|
+
`window.__HUB_LOGIN__='${escapeJsString(this.config.loginEndpoint)}';`,
|
|
47
|
+
authHeader ? `window.__HUB_TOKEN__='${escapeJsString(authHeader)}';` : '',
|
|
48
|
+
].join('');
|
|
49
|
+
|
|
50
|
+
// SECURITY (self-sufficient login): the shell is PUBLIC because it must render the login form
|
|
51
|
+
// before authentication. It therefore carries NO cockpit chrome — no navigation, no environment
|
|
52
|
+
// badge, no version, no external links. The client builds all of that ONLY after a successful
|
|
53
|
+
// ADMIN auth check, from the ADMIN-gated `session.json` payload. So an unauthenticated request to
|
|
54
|
+
// any /hub page reveals nothing of the Hub's structure; #hub-app stays empty until login.
|
|
55
|
+
return `<!DOCTYPE html>
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="UTF-8">
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
60
|
+
<title>Hub · nest-server</title>
|
|
61
|
+
<style nonce="${HUB_NONCE_PLACEHOLDER}">${this.css()}</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body class="hub-locked">
|
|
64
|
+
<div id="hub-gate" class="hub-gate"></div>
|
|
65
|
+
<div id="hub-app"></div>
|
|
66
|
+
<dialog id="hub-confirm" class="hub-dialog"></dialog>
|
|
67
|
+
<script nonce="${HUB_NONCE_PLACEHOLDER}">${bootstrap}</script>
|
|
68
|
+
<script nonce="${HUB_NONCE_PLACEHOLDER}" src="${escapeHtml(base)}/hub.js?v=${HUB_ASSET_TOKEN}"></script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected css(): string {
|
|
74
|
+
return `
|
|
75
|
+
:root{--bg:#0b0d12;--panel:#141821;--panel2:#1b212d;--border:#242c3a;--text:#e6ebf2;--muted:#8b97a8;--accent:#4f8cff;--ok:#3fb950;--warn:#d29922;--err:#f85149}
|
|
76
|
+
*{box-sizing:border-box}
|
|
77
|
+
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}
|
|
78
|
+
.hub-layout{display:flex;min-height:100vh}
|
|
79
|
+
.hub-nav{width:230px;flex:0 0 230px;background:var(--panel);border-right:1px solid var(--border);padding:16px 10px;overflow-y:auto}
|
|
80
|
+
.hub-brand{font-weight:700;font-size:18px;padding:4px 10px 14px;display:flex;align-items:center;gap:8px}
|
|
81
|
+
.hub-env{font-size:10px;text-transform:uppercase;letter-spacing:.05em;padding:2px 6px;border-radius:4px;background:var(--panel2);color:var(--muted)}
|
|
82
|
+
.hub-env-production{background:#3d1418;color:#ff7b72}
|
|
83
|
+
.hub-nav h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:16px 10px 4px}
|
|
84
|
+
.hub-nav a{display:block;padding:6px 10px;border-radius:6px;color:var(--text);text-decoration:none;font-size:13px}
|
|
85
|
+
.hub-nav a:hover{background:var(--panel2)}
|
|
86
|
+
.hub-nav a.active{background:var(--accent);color:#fff}
|
|
87
|
+
.hub-nav a.hub-nav-disabled{opacity:.38;cursor:not-allowed}
|
|
88
|
+
.hub-nav a.hub-nav-disabled:hover{background:none}
|
|
89
|
+
.hub-nav a.hub-ext::after{content:' ↗';color:var(--muted)}
|
|
90
|
+
.hub-main{flex:1;min-width:0;display:flex;flex-direction:column}
|
|
91
|
+
.hub-topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 22px;border-bottom:1px solid var(--border)}
|
|
92
|
+
.hub-topbar h1{font-size:18px;margin:0}
|
|
93
|
+
.hub-meta{color:var(--muted);font-size:12px;display:flex;align-items:center;gap:8px}
|
|
94
|
+
.hub-dot{width:9px;height:9px;border-radius:50%;display:inline-block;background:var(--muted)}
|
|
95
|
+
.hub-dot-ok{background:var(--ok)}.hub-dot-err{background:var(--err)}
|
|
96
|
+
.hub-content{padding:22px;overflow:auto}
|
|
97
|
+
.hub-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px}
|
|
98
|
+
.hub-tile{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:14px}
|
|
99
|
+
.hub-tile-label{color:var(--muted);font-size:12px}
|
|
100
|
+
.hub-tile-value{font-size:22px;font-weight:600;margin-top:4px}
|
|
101
|
+
.hub-tile-value.ok{color:var(--ok)}.hub-tile-value.warn{color:var(--warn)}.hub-tile-value.err{color:var(--err)}
|
|
102
|
+
.hub-tile-sub{color:var(--muted);font-size:11px;margin-top:4px}
|
|
103
|
+
.hub-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden}
|
|
104
|
+
.hub-table th{text-align:left;padding:8px 12px;background:var(--panel2);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer}
|
|
105
|
+
.hub-table td{padding:8px 12px;border-top:1px solid var(--border);font-size:13px}
|
|
106
|
+
.hub-table tr:hover td{background:var(--panel2)}
|
|
107
|
+
.hub-json{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:14px;overflow:auto;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre;max-width:100%}
|
|
108
|
+
.hub-empty{color:var(--muted);padding:40px;text-align:center;border:1px dashed var(--border);border-radius:8px}
|
|
109
|
+
.hub-btn{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer}
|
|
110
|
+
.hub-btn:hover{border-color:var(--accent)}
|
|
111
|
+
.hub-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
112
|
+
.hub-btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
113
|
+
.hub-btn-danger{background:var(--err);border-color:var(--err);color:#fff}
|
|
114
|
+
.hub-signout{padding:4px 10px;font-size:12px;margin-left:4px}
|
|
115
|
+
.hub-input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px;color:var(--text);margin:8px 0;font-size:13px}
|
|
116
|
+
.hub-dialog{background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:440px;width:90%}
|
|
117
|
+
.hub-dialog::backdrop{background:rgba(0,0,0,.6)}
|
|
118
|
+
.hub-dialog h3{margin:0 0 8px}
|
|
119
|
+
.hub-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
|
|
120
|
+
.hub-hint{color:var(--muted);font-size:12px}
|
|
121
|
+
.hub-chip{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;background:var(--panel2);color:var(--muted)}
|
|
122
|
+
.hub-chip-ok{background:#132d1c;color:var(--ok)}.hub-chip-warn{background:#2d2611;color:var(--warn)}.hub-chip-err{background:#3d1418;color:var(--err)}
|
|
123
|
+
.hub-toast{position:fixed;bottom:20px;right:20px;background:var(--panel2);border:1px solid var(--accent);color:var(--text);padding:12px 16px;border-radius:8px;font-size:13px;z-index:9999;box-shadow:0 6px 20px rgba(0,0,0,.4)}
|
|
124
|
+
.hub-toast-err{border-color:var(--err)}
|
|
125
|
+
/* Auth gate: nothing of the app is visible until authenticated. The client builds the layout into
|
|
126
|
+
#hub-app only after a successful auth check, so body.hub-locked just hides the (empty) app root. */
|
|
127
|
+
.hub-gate{display:none;position:fixed;inset:0;background:var(--bg);z-index:100;align-items:center;justify-content:center;padding:20px}
|
|
128
|
+
.hub-locked .hub-gate{display:flex}
|
|
129
|
+
.hub-locked #hub-app{display:none}
|
|
130
|
+
.hub-gate-card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:28px;width:360px;max-width:100%}
|
|
131
|
+
.hub-gate-card h2{margin:0 0 4px}
|
|
132
|
+
.hub-gate-brand{font-weight:700;font-size:20px;margin-bottom:18px;display:flex;align-items:center;gap:8px}
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
}
|