bhooai-nexus 2.0.10 → 2.0.12
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/README.md +50 -0
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/CONVENTIONS.md +60 -10
- package/package.json +4 -1
- package/packages/nexus-admin/package.json +1 -1
- package/packages/nexus-admin/src/App.tsx +111 -9
- package/packages/nexus-ads/package.json +1 -1
- package/packages/nexus-ai-client/package.json +1 -1
- package/packages/nexus-auth/package.json +1 -1
- package/packages/nexus-auth/src/csrf.ts +23 -0
- package/packages/nexus-cache/package.json +1 -1
- package/packages/nexus-cli/package.json +1 -1
- package/packages/nexus-cli/src/commands/add.ts +123 -59
- package/packages/nexus-cli/src/commands/init.ts +173 -9
- package/packages/nexus-cli/src/devPanel.ts +275 -121
- package/packages/nexus-cli/src/devServiceManager.ts +26 -12
- package/packages/nexus-cli/src/dispatcher.ts +1 -1
- package/packages/nexus-cli/src/launcher.ts +11 -10
- package/packages/nexus-cli/src/layout.ts +45 -5
- package/packages/nexus-cli/src/make/generators.ts +125 -9
- package/packages/nexus-cli/src/templating/tree.ts +4 -2
- package/packages/nexus-cli/src/tui.ts +23 -1
- package/packages/nexus-cli/src/util.ts +11 -5
- package/packages/nexus-cli/src/wizard.ts +33 -36
- package/packages/nexus-cli/templates/base/apps/admin/nginx.conf.ejs +5 -0
- package/packages/nexus-cli/templates/base/apps/admin/vite.config.ts.ejs +4 -0
- package/packages/nexus-cli/templates/base/apps/frontend/nginx.conf.ejs +5 -0
- package/packages/nexus-cli/templates/base/apps/frontend/vite.config.ts.ejs +4 -0
- package/packages/nexus-cli/templates/base/package.json.ejs +2 -1
- package/packages/nexus-cli/templates/features/realtime/apps/backend/src/ws/chat.room.ts +34 -35
- package/packages/nexus-core/package.json +1 -1
- package/packages/nexus-core/src/app/createNexusApp.ts +313 -20
- package/packages/nexus-core/src/app/discover.ts +39 -8
- package/packages/nexus-core/src/config/defaults.ts +4 -0
- package/packages/nexus-core/src/config/schema.ts +4 -0
- package/packages/nexus-core/src/config/types.ts +8 -0
- package/packages/nexus-crypto/package.json +1 -1
- package/packages/nexus-data/package.json +1 -1
- package/packages/nexus-email/package.json +1 -1
- package/packages/nexus-examples/examples/ai-chat/apps/backend/src/handlers/chat.handler.ts +27 -0
- package/packages/nexus-examples/examples/ai-chat/apps/backend/src/middleware/validateChat.ts +3 -0
- package/packages/nexus-examples/examples/ai-chat/apps/backend/src/routes/chat.ts +5 -38
- package/packages/nexus-examples/examples/ai-chat/apps/backend/src/types/ai.chat.types.ts +7 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/graphql/app.graph.ts +89 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/handlers/blog.handler.ts +63 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/middleware/validatePost.ts +15 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/repositories/PostRepository.ts +20 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/services/PostService.ts +41 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/types/blog.author.types.ts +16 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/types/blog.comment.types.ts +15 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/types/blog.post.types.ts +39 -0
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/validators/post.schema.ts +9 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/graphql/app.graph.ts +59 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/handlers/chat.handler.ts +22 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/middleware/validateMessage.ts +11 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/routes/index.ts +22 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/services/ChatService.ts +14 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/types/chat.message.types.ts +23 -0
- package/packages/nexus-examples/examples/chat/apps/backend/src/ws/chat.room.ts +18 -45
- package/packages/nexus-examples/examples/chat/apps/frontend/src/main.tsx +58 -16
- package/packages/nexus-examples/examples/checkout/apps/backend/src/graphql/app.graph.ts +45 -0
- package/packages/nexus-examples/examples/checkout/apps/backend/src/handlers/checkout.handler.ts +36 -0
- package/packages/nexus-examples/examples/checkout/apps/backend/src/mail/templates/order-confirmation.ejs +3 -3
- package/packages/nexus-examples/examples/checkout/apps/backend/src/middleware/validateOrder.ts +3 -0
- package/packages/nexus-examples/examples/checkout/apps/backend/src/routes/checkout.ts +5 -70
- package/packages/nexus-examples/examples/checkout/apps/backend/src/types/checkout.order.types.ts +7 -0
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/graphql/app.graph.ts +38 -0
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/handlers/metrics.handler.ts +42 -0
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/middleware/validateMetric.ts +3 -0
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/routes/metrics.ts +6 -70
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/types/dashboard.metric.types.ts +7 -0
- package/packages/nexus-examples/examples/file-storage/apps/backend/src/graphql/app.graph.ts +46 -0
- package/packages/nexus-examples/examples/file-storage/apps/backend/src/handlers/file.handler.ts +45 -0
- package/packages/nexus-examples/examples/file-storage/apps/backend/src/middleware/validateFile.ts +3 -0
- package/packages/nexus-examples/examples/file-storage/apps/backend/src/routes/files.ts +8 -82
- package/packages/nexus-examples/examples/file-storage/apps/backend/src/types/file.file.types.ts +7 -0
- package/packages/nexus-examples/examples/livestream/apps/backend/src/middleware/validateStream.ts +3 -0
- package/packages/nexus-examples/examples/livestream/apps/backend/src/types/live.stream.types.ts +7 -0
- package/packages/nexus-examples/examples/multi-app/_setup.mjs +39 -0
- package/packages/nexus-examples/examples/multi-app/apps/backend-api/src/routes/users.ts +18 -1
- package/packages/nexus-examples/examples/multi-app/apps/backend-shop/src/routes/products.ts +18 -1
- package/packages/nexus-examples/examples/multi-app/apps/frontend-shop/src/main.tsx +63 -0
- package/packages/nexus-examples/examples/multi-app/apps/frontend-web/src/main.tsx +70 -0
- package/packages/nexus-examples/examples/multi-app/apps/frontend-web/vite.config.ts +17 -0
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/events/TeamCreated.ts +1 -7
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/events/UserRegistered.ts +1 -6
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/graphql/app.graph.ts +109 -0
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/handlers/project.handler.ts +32 -0
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/handlers/team.handler.ts +75 -0
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/listeners/OnTeamCreated.ts +3 -6
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/listeners/OnUserRegistered.ts +4 -5
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/middleware/auth.ts +4 -6
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/models/Membership.ts +6 -9
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/models/User.ts +0 -2
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/policies/TeamPolicy.ts +0 -9
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/services/TeamService.ts +19 -0
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/types/saas.team.types.ts +10 -0
- package/packages/nexus-examples/examples/video-call/apps/backend/src/middleware/validateCall.ts +3 -0
- package/packages/nexus-examples/examples/video-call/apps/backend/src/types/video.call.types.ts +7 -0
- package/packages/nexus-examples/package.json +1 -1
- package/packages/nexus-graphql/package.json +6 -1
- package/packages/nexus-graphql/src/builtin/helloSubgraph.ts +38 -0
- package/packages/nexus-graphql/src/builtin/index.ts +3 -0
- package/packages/nexus-graphql/src/explorer/explorerHtml.ts +1495 -0
- package/packages/nexus-graphql/src/federation/executor.ts +20 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +13 -11
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +31 -4
- package/packages/nexus-graphql/src/index.ts +4 -1
- package/packages/nexus-graphql/src/mutationCsrf.ts +25 -0
- package/packages/nexus-payments/package.json +1 -1
- package/packages/nexus-plugins/package.json +1 -1
- package/packages/nexus-postcss/package.json +1 -1
- package/packages/nexus-realtime/package.json +1 -1
- package/packages/nexus-safe-goto/package.json +1 -1
- package/packages/nexus-safe-goto/src/index.tsx +1 -1
- package/packages/nexus-telemetry/package.json +1 -1
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/graphql/post.graph.ts +0 -99
- package/packages/nexus-examples/examples/blog-crud/apps/backend/src/routes/posts.ts +0 -119
- package/packages/nexus-examples/examples/chat/apps/backend/src/routes/chat.ts +0 -17
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/routes/projects.ts +0 -52
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/routes/teams.ts +0 -112
package/README.md
CHANGED
|
@@ -192,6 +192,56 @@ seeder · migration · provider · plugin
|
|
|
192
192
|
| `file-storage` | local + S3 storage, signed private URLs |
|
|
193
193
|
| `multi-app` | two backends, two frontends, one admin |
|
|
194
194
|
|
|
195
|
+
### Multi-app example — how to use
|
|
196
|
+
|
|
197
|
+
Nexus as a platform: two backends (`shop` + `api`) and two frontends (`web` + `shop`) in one project, single admin. The overlay ships only two route files; actual `apps/` are scaffolded via `nexus add`.
|
|
198
|
+
|
|
199
|
+
**Option A — fresh project (wizard):**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx nexus init my-platform
|
|
203
|
+
# wizard: Starter -> Examples -> pick "multi-app" -> Admin? Yes -> Features/Ports/Infra -> Review & create
|
|
204
|
+
cd my-platform
|
|
205
|
+
|
|
206
|
+
# scaffold the extra backends (ports auto-assigned in .nexus-ports.json)
|
|
207
|
+
npx nexus add backend shop
|
|
208
|
+
npx nexus add backend api
|
|
209
|
+
|
|
210
|
+
# wire frontends to chosen backends (--for sets Vite proxy /api, /uploads, /ws)
|
|
211
|
+
npx nexus add frontend shop --for backend-shop
|
|
212
|
+
npx nexus add frontend web --for backend
|
|
213
|
+
|
|
214
|
+
npx nexus dev
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Or non-interactively: `npx nexus init my-platform --example multi-app` then run the same `add` commands.
|
|
218
|
+
|
|
219
|
+
**Option B — existing project:**
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx nexus add backend shop
|
|
223
|
+
npx nexus add backend api
|
|
224
|
+
# if project wasn't inited with multi-app, copy the two demo routes manually:
|
|
225
|
+
# apps/backend-shop/src/routes/products.ts -> GET /api/products, GET /api/products/:id
|
|
226
|
+
# apps/backend-api/src/routes/users.ts -> GET /api/users, GET /api/users/:id
|
|
227
|
+
npx nexus add frontend shop --for backend-shop
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**What you end up with:**
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
apps/
|
|
234
|
+
backend/ # default backend from init
|
|
235
|
+
backend-shop/ # storefront API -> routes/products.ts
|
|
236
|
+
backend-api/ # shared API -> routes/users.ts
|
|
237
|
+
frontend/ # default frontend
|
|
238
|
+
frontend-shop/ # proxies to backend-shop
|
|
239
|
+
frontend-web/ # proxies to backend
|
|
240
|
+
admin/ # single admin aggregating health/logs across all backends
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Each `apps/backend-*` is an isolated `createNexusApp()` with its own `src/routes`, `src/ws`, `src/models`, etc. auto-discovered. Verify with `curl http://localhost:4010/api/products` and `curl http://localhost:4011/api/users` after `nexus dev` prints the port map. See `packages/nexus-examples/examples/multi-app/README.md`.
|
|
244
|
+
|
|
195
245
|
## Storage
|
|
196
246
|
|
|
197
247
|
Per-backend `storage/` folder with `local` + `s3` drivers shipped:
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -60,7 +60,7 @@ the registry via `lookupPort()` at scaffold time and bake the port into
|
|
|
60
60
|
|
|
61
61
|
1. `loadConfigAuto()` — config stack: code defaults < nexus.config.ts <
|
|
62
62
|
nexus.runtime.json < NEXUS_* env vars < CLI flags
|
|
63
|
-
2. `discoverBackend(srcRoot)` — walks 14 folder patterns:
|
|
63
|
+
2. `discoverBackend(srcRoot)` — walks 14 flat folder patterns:
|
|
64
64
|
|
|
65
65
|
| Folder | Pattern | Used as |
|
|
66
66
|
|-----------------|----------------------------------|---------|
|
|
@@ -81,6 +81,8 @@ the registry via `lookupPort()` at scaffold time and bake the port into
|
|
|
81
81
|
| plugins/<n>/ | `index.ts` | Local plugins |
|
|
82
82
|
| config/ | `*.ts` | Merged into app config |
|
|
83
83
|
|
|
84
|
+
Flat workflow with dot-namespaced types: `src/types/blog.post.types.ts`, `src/middleware/validatePost.ts`, `src/models/Post.ts`, `src/services/PostService.ts`. Example: `blog-crud` uses `src/types/blog.post.types.ts` + `src/middleware/validatePost.ts` + `src/routes/posts.ts` (thin → `PostService`). `discover` also still scans legacy `src/modules/*/…` via `collectWithModules` for backward compat.
|
|
85
|
+
|
|
84
86
|
3. Configures cross-cutting services:
|
|
85
87
|
- `configureStorageFromEnv()` — Storage facade with `local` + `s3` disks
|
|
86
88
|
- `configureQueue()` — Job queue adapter (in-memory default, Redis-ready)
|
package/docs/CONVENTIONS.md
CHANGED
|
@@ -49,6 +49,26 @@ export const prefix = '/api/v2';
|
|
|
49
49
|
|
|
50
50
|
Optional per-route fields: `middleware`, `upload`, `rateLimit`, `public`, `request`.
|
|
51
51
|
|
|
52
|
+
Routes are **thin** — handlers live in `src/handlers/`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// handlers/chat.handler.ts
|
|
56
|
+
import type { RequestContext } from '@bhooai/nexus-core';
|
|
57
|
+
import { Message } from '../models/Message.js';
|
|
58
|
+
export async function listMessages(ctx: RequestContext) { /* ... */ }
|
|
59
|
+
export async function sendMessage(ctx: RequestContext) { /* ... */ }
|
|
60
|
+
|
|
61
|
+
// routes/chat.ts
|
|
62
|
+
import { defineRoutes } from '@bhooai/nexus-core';
|
|
63
|
+
import { validateMessage } from '../middleware/validateMessage.js';
|
|
64
|
+
import { listMessages, sendMessage } from '../handlers/chat.handler.js';
|
|
65
|
+
export default defineRoutes([
|
|
66
|
+
{ method:'GET', path:'/api/chat/rooms/:roomId/messages', handler: listMessages },
|
|
67
|
+
{ method:'POST', path:'/api/chat/rooms/:roomId/messages', middleware:[validateMessage], handler: sendMessage },
|
|
68
|
+
]);
|
|
69
|
+
```
|
|
70
|
+
Generate with `nexus make:handler chat` → `src/handlers/chat.handler.ts` (`handlers/<name>.handler.ts` — not auto-discovered, imported by routes).
|
|
71
|
+
|
|
52
72
|
## graphql/
|
|
53
73
|
|
|
54
74
|
Files matching `*.graph.ts` export typeDefs + resolvers:
|
|
@@ -155,21 +175,51 @@ Run with `nexus queue:work --queue mail`.
|
|
|
155
175
|
`register(container)` wires DI singletons; `boot(container)` runs async init
|
|
156
176
|
after all providers are registered.
|
|
157
177
|
|
|
158
|
-
##
|
|
178
|
+
## handlers/ — route handlers (separate folder)
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
They exist so your routes stay thin:
|
|
180
|
+
`src/handlers/<name>.handler.ts` exports plain `Handler` functions (`(ctx: RequestContext)=>...`). **Not auto-discovered** — routes import them. Prefer one file per domain (`chat.handler.ts` with `listMessages, sendMessage`, `blog.handler.ts` with `listPosts, createPost, ...`). Keeps `routes/` declarative (10–20 LOC) and testable.
|
|
162
181
|
|
|
163
182
|
```
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
→ requests/CreateUserRequest.ts (zod + authorize)
|
|
170
|
-
→ resources/UserResource.ts (response shape)
|
|
183
|
+
src/handlers/
|
|
184
|
+
chat.handler.ts # listMessages, sendMessage
|
|
185
|
+
blog.handler.ts # listPosts, createPost, getPost, updatePost, deletePost, createComment
|
|
186
|
+
checkout.handler.ts # createOrder, webhook
|
|
187
|
+
file.handler.ts # listFiles, uploadPublic, …
|
|
171
188
|
```
|
|
172
189
|
|
|
190
|
+
## models/, services/, repositories/, controllers/, middleware/, validators/, requests/, resources/, types/ — flat src/<folder> (canonical)
|
|
191
|
+
|
|
192
|
+
These are **not auto-wired** — import them yourself from `routes/handlers/jobs/listeners`. They exist so routes+handlers stay thin. Use **flat** `src/<folder>` with **dot-namespaced types** for domain separation (no extra `modules/` folder):
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
src/
|
|
196
|
+
models/{Post.ts, User.ts, Team.ts} # ODM — model('posts', schema)
|
|
197
|
+
types/blog.post.types.ts # DTOs + Zod (PostDto, CreatePostInput) — dot namespaced
|
|
198
|
+
types/blog.author.types.ts
|
|
199
|
+
types/chat.message.types.ts
|
|
200
|
+
types/saas.team.types.ts
|
|
201
|
+
types/checkout.order.types.ts
|
|
202
|
+
routes/posts.ts # defineRoutes (thin → services)
|
|
203
|
+
services/PostService.ts # business logic
|
|
204
|
+
repositories/PostRepository.ts # Mongo queries (Db)
|
|
205
|
+
middleware/validatePost.ts # Middleware (ctx,next)
|
|
206
|
+
middleware/validateMessage.ts
|
|
207
|
+
middleware/auth.ts
|
|
208
|
+
validators/post.schema.ts # Zod
|
|
209
|
+
graphql/post.graph.ts # defineSubgraph (imports ../models/Post.js)
|
|
210
|
+
ws/chat.room.ts
|
|
211
|
+
|
|
212
|
+
routes/posts.ts → services/PostService.ts
|
|
213
|
+
→ repositories/PostRepository.ts
|
|
214
|
+
→ models/Post.ts
|
|
215
|
+
→ types/blog.post.types.ts # `import { CreatePostInput } from '../types/blog.post.types.js'`
|
|
216
|
+
→ validators/post.schema.ts
|
|
217
|
+
→ middleware/validatePost.ts # `import { validatePost } from '../middleware/validatePost.js'`
|
|
218
|
+
→ resources/PostResource.ts
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Generate with flat commands: `nexus make:model Post`, `nexus make:types blog.post`, `nexus make:middleware validatePost`, `nexus make:route posts`, `nexus make:service PostService`. No `--module` flag. Legacy `src/modules/*` is still discovered via `collectWithModules` (`discover.ts:115`) for backward compat but not recommended.
|
|
222
|
+
|
|
173
223
|
## database/
|
|
174
224
|
|
|
175
225
|
- `seeds/*Seeder.ts` — classes extending `Seeder`. `nexus db:seed`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bhooai-nexus",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
4
4
|
"description": "BhooAI Nexus - a Node-based full-stack framework (websockets, GraphQL federation, inbuilt security, custom ODM, OAuth, payments, WebRTC, AI, plugins). v2: multi-app, Laravel-style folders, Docker-based scaling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BhooAI",
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
"./postcss": "./packages/nexus-postcss/src/index.js",
|
|
57
57
|
"./postcss/theme.css": "./packages/nexus-postcss/src/theme.css",
|
|
58
58
|
"./examples": "./packages/nexus-examples/src/index.ts",
|
|
59
|
+
"./graphql": "./packages/nexus-graphql/src/index.ts",
|
|
60
|
+
"./graphql/hello": "./packages/nexus-graphql/src/builtin/helloSubgraph.ts",
|
|
61
|
+
"./graphql/builtin": "./packages/nexus-graphql/src/builtin/index.ts",
|
|
59
62
|
"./package.json": "./package.json"
|
|
60
63
|
},
|
|
61
64
|
"files": [
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from './api.js';
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'schema' | 'ai' | 'theme';
|
|
22
|
+
type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'schema' | 'ai' | 'graphql' | 'theme';
|
|
23
23
|
type AdminTheme = 'aurora' | 'midnight' | 'violet';
|
|
24
24
|
type UiMode = 'workspace' | 'compact' | 'focus';
|
|
25
25
|
type ToggleStyle = 'chip' | 'neon' | 'icon' | 'orb' | 'track';
|
|
@@ -459,6 +459,7 @@ function Dashboard({ user, onUserChange, onLogout }: { user: AdminUser | null; o
|
|
|
459
459
|
{ id: 'databases', label: 'Databases', icon: 'database', keywords: ['mongo', 'mongodb', 'collections', 'data', 'tables', 'docs'], desc: 'Databases, collections and their documents.' },
|
|
460
460
|
{ id: 'schema', label: 'AI Schema', icon: 'sparkles', keywords: ['generate', 'ai', 'model', 'json schema', 'validator'], desc: 'Generate MongoDB schemas from plain English.' },
|
|
461
461
|
{ id: 'ai', label: 'AI Agents', icon: 'bot', keywords: ['providers', 'chat', 'playground', 'ollama', 'openai', 'models', 'settings'], desc: 'AI settings, providers and chat playground.' },
|
|
462
|
+
{ id: 'graphql', label: 'GraphQL', icon: 'gitFork', keywords: ['graphiql', 'explorer', 'schema', 'playground', 'graphql', 'api'], desc: 'GraphQL explorer and access control.' },
|
|
462
463
|
{ id: 'theme', label: 'Theme Centre', icon: 'palette', keywords: ['appearance', 'colors', 'palette', 'ui', 'customize', 'surface'], desc: 'Curated themes, palettes and surface tuning.' },
|
|
463
464
|
];
|
|
464
465
|
const searchResults = useMemo(() => {
|
|
@@ -645,6 +646,7 @@ function Dashboard({ user, onUserChange, onLogout }: { user: AdminUser | null; o
|
|
|
645
646
|
{tab === 'databases' && <Databases />}
|
|
646
647
|
{tab === 'schema' && <Schema />}
|
|
647
648
|
{tab === 'ai' && <AiAgents toggle={customTheme.toggle} />}
|
|
649
|
+
{tab === 'graphql' && <GraphQLSettings onNavigate={setTab} />}
|
|
648
650
|
{tab === 'theme' && <ThemeCentre theme={theme} onThemeChange={(value) => { setTheme(value); window.localStorage.setItem('nexus-admin-theme', value); }} uiMode={uiMode} onUiModeChange={(value) => { setUiMode(value); window.localStorage.setItem('nexus-admin-ui', value); }} custom={customTheme} onCustom={updateCustomTheme} particles={particles} onParticles={updateParticles} />}
|
|
649
651
|
</main>
|
|
650
652
|
<aside className="admin-inspector">
|
|
@@ -1771,12 +1773,12 @@ function Logs({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
|
|
|
1771
1773
|
return (
|
|
1772
1774
|
<div className="space-y-6">
|
|
1773
1775
|
<PageHead title="Logs">
|
|
1774
|
-
<div className="flex items-center gap-2">
|
|
1775
|
-
<button aria-
|
|
1776
|
-
<button aria-
|
|
1777
|
-
<button aria-label="Copy filtered logs" onClick={() => void copyAll()} className="glass-chip-btn">{copied ? 'Copied' : <><Icon name="copy" size={12} /> copy</>}</button>
|
|
1778
|
-
<button aria-label="Summarize logs with AI" onClick={() => void runAiSummary()} disabled={aiSummaryBusy} className="glass-chip-btn log-ai-btn"><Icon name="sparkles" size={12} /> {aiSummaryBusy ? 'summarizing…' : 'ai summary'}</button>
|
|
1779
|
-
<button aria-label="Clear all logs" onClick={async () => { try { await clearLogs(); await refresh(); } catch { /* supervisor may not be reachable */ } }} className="glass-chip-btn-danger"><Icon name="trash" size={12} /> clear</button>
|
|
1776
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1777
|
+
<button aria-pressed={live} onClick={() => setLive(!live)} className={`glass-chip-btn inline-flex items-center gap-1.5 log-live-btn${live ? ' is-live' : ''}`}>{live ? '● live' : '○ paused'}</button>
|
|
1778
|
+
<button aria-label="Refresh logs" onClick={() => void refresh()} className="glass-chip-btn inline-flex items-center gap-1.5"><Icon name="refresh" size={12} /> refresh</button>
|
|
1779
|
+
<button aria-label="Copy filtered logs" onClick={() => void copyAll()} className="glass-chip-btn inline-flex items-center gap-1.5">{copied ? 'Copied' : <><Icon name="copy" size={12} /> copy</>}</button>
|
|
1780
|
+
<button aria-label="Summarize logs with AI" onClick={() => void runAiSummary()} disabled={aiSummaryBusy} className="glass-chip-btn log-ai-btn inline-flex items-center gap-1.5"><Icon name="sparkles" size={12} /> {aiSummaryBusy ? 'summarizing…' : 'ai summary'}</button>
|
|
1781
|
+
<button aria-label="Clear all logs" onClick={async () => { try { await clearLogs(); await refresh(); } catch { /* supervisor may not be reachable */ } }} className="glass-chip-btn-danger inline-flex items-center gap-1.5"><Icon name="trash" size={12} /> clear</button>
|
|
1780
1782
|
</div>
|
|
1781
1783
|
</PageHead>
|
|
1782
1784
|
<PageHero kicker="ERROR CONSOLE" title={<>Every failure, <em>in one stream.</em></>} desc="Aggregated stdout, stderr and supervisor events from every service — live as the supervisor records them, filterable and searchable." glyph="terminal" art="wave" />
|
|
@@ -2173,7 +2175,7 @@ function AiProviders({ toggle }: { toggle: ToggleStyle }) {
|
|
|
2173
2175
|
const [showAdd, setShowAdd] = useState(false);
|
|
2174
2176
|
const [newProvider, setNewProvider] = useState({ id: '', label: '', baseUrl: '', apiKey: '', defaultModel: '' });
|
|
2175
2177
|
const [query, setQuery] = useState('');
|
|
2176
|
-
const [filter, setFilter] = useState<ProviderFilter>('
|
|
2178
|
+
const [filter, setFilter] = useState<ProviderFilter>('all');
|
|
2177
2179
|
const [testingId, setTestingId] = useState<string | null>(null);
|
|
2178
2180
|
const [testResults, setTestResults] = useState<Record<string, AiProviderTestResult>>({});
|
|
2179
2181
|
const [keyEditorProvider, setKeyEditorProvider] = useState<AiProviderView | null>(null);
|
|
@@ -2185,7 +2187,14 @@ function AiProviders({ toggle }: { toggle: ToggleStyle }) {
|
|
|
2185
2187
|
const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));
|
|
2186
2188
|
|
|
2187
2189
|
const loadProviders = async () => {
|
|
2188
|
-
try {
|
|
2190
|
+
try {
|
|
2191
|
+
const r = await getAiProviders();
|
|
2192
|
+
setProviders(r.providers ?? []);
|
|
2193
|
+
if (!r.providers?.length) setMsg('No providers returned — is the backend running and are you logged in as admin?');
|
|
2194
|
+
else setMsg('');
|
|
2195
|
+
} catch (e: any) {
|
|
2196
|
+
setMsg(`Failed to load providers: ${String(e?.message ?? e)} — ensure you are logged in as admin (403 = not admin, 401 = not logged in).`);
|
|
2197
|
+
}
|
|
2189
2198
|
};
|
|
2190
2199
|
|
|
2191
2200
|
const persistenceNote = (persistence?: AiProviderPersistence): string => {
|
|
@@ -2349,6 +2358,11 @@ function AiProviders({ toggle }: { toggle: ToggleStyle }) {
|
|
|
2349
2358
|
<button onClick={() => setShowAdd(true)} disabled={busy} className="glass-chip-btn">+ add provider</button>
|
|
2350
2359
|
</div>
|
|
2351
2360
|
</div>
|
|
2361
|
+
{msg && (
|
|
2362
|
+
<div role="status" className={`rounded-lg border px-3 py-2 text-xs ${msg.includes('Failed') || msg.includes('No providers') ? 'border-amber-500/30 bg-amber-500/10 text-amber-300' : 'border-sky-400/20 bg-sky-400/10 text-sky-300'}`}>
|
|
2363
|
+
{msg}
|
|
2364
|
+
</div>
|
|
2365
|
+
)}
|
|
2352
2366
|
|
|
2353
2367
|
{/* Filter bar */}
|
|
2354
2368
|
<div className="provider-filter-bar">
|
|
@@ -3229,6 +3243,94 @@ function Plugins() {
|
|
|
3229
3243
|
);
|
|
3230
3244
|
}
|
|
3231
3245
|
|
|
3246
|
+
function GraphQLSettings({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
|
|
3247
|
+
const [cfg, setCfg] = useState<AdminConfig | null>(null);
|
|
3248
|
+
const [explorer, setExplorer] = useState(true);
|
|
3249
|
+
const [requireAuth, setRequireAuth] = useState(false);
|
|
3250
|
+
const [msg, setMsg] = useAdminAlert('graphql');
|
|
3251
|
+
const [busy, setBusy] = useState(false);
|
|
3252
|
+
const [restartDialog, setRestartDialog] = useState(false);
|
|
3253
|
+
|
|
3254
|
+
const load = async () => {
|
|
3255
|
+
try {
|
|
3256
|
+
const c = await getAdminConfig();
|
|
3257
|
+
setCfg(c);
|
|
3258
|
+
const g = (c.config?.graphql ?? {}) as Record<string, unknown>;
|
|
3259
|
+
setExplorer(g.explorer !== false);
|
|
3260
|
+
setRequireAuth(!!g.explorerRequireAuth);
|
|
3261
|
+
} catch (e: any) { setMsg(String(e?.message ?? e)); }
|
|
3262
|
+
};
|
|
3263
|
+
useEffect(() => { load(); }, []);
|
|
3264
|
+
|
|
3265
|
+
const save = async () => {
|
|
3266
|
+
setBusy(true); setMsg('');
|
|
3267
|
+
try {
|
|
3268
|
+
await putAdminConfig({ graphql: { explorer, explorerRequireAuth: requireAuth } });
|
|
3269
|
+
setMsg('Saved to nexus.runtime.json. Restart services to apply changes.');
|
|
3270
|
+
setRestartDialog(true);
|
|
3271
|
+
} catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); }
|
|
3272
|
+
finally { setBusy(false); }
|
|
3273
|
+
};
|
|
3274
|
+
|
|
3275
|
+
const g = (cfg?.config?.graphql ?? {}) as Record<string, unknown>;
|
|
3276
|
+
const path = (g.path as string) ?? '/graphql';
|
|
3277
|
+
const introspection = g.introspection !== false;
|
|
3278
|
+
|
|
3279
|
+
return (
|
|
3280
|
+
<div className="space-y-6">
|
|
3281
|
+
<PageHead title="GraphQL" />
|
|
3282
|
+
<PageHero kicker="GRAPHQL WORKSPACE" title={<>Explore your <em>schema.</em></>} desc="Control the GraphiQL explorer and gate it behind an admin access token." glyph="gitFork" art="grid" />
|
|
3283
|
+
|
|
3284
|
+
<section className="glass-card space-y-3">
|
|
3285
|
+
<h3 className="glass-h3">GraphiQL explorer</h3>
|
|
3286
|
+
<p className="text-xs text-slate-500">The interactive sandbox is served at <code className="font-mono text-slate-300">{path}</code> (page) and <code className="font-mono text-slate-300">/graphiql</code>. The <code className="font-mono text-slate-300">/graphql</code> JSON endpoint is always public.</p>
|
|
3287
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
3288
|
+
<div className="glass flex items-center justify-between rounded-xl px-3 py-2">
|
|
3289
|
+
<div>
|
|
3290
|
+
<span className="admin-overline">EXPLORER</span>
|
|
3291
|
+
<div className="text-sm text-slate-200">Enable GraphiQL</div>
|
|
3292
|
+
</div>
|
|
3293
|
+
<button onClick={() => setExplorer(!explorer)} className={`glass-chip-btn ${explorer ? 'is-on' : ''}`}>{explorer ? 'On' : 'Off'}</button>
|
|
3294
|
+
</div>
|
|
3295
|
+
<div className="glass flex items-center justify-between rounded-xl px-3 py-2">
|
|
3296
|
+
<div>
|
|
3297
|
+
<span className="admin-overline">ACCESS</span>
|
|
3298
|
+
<div className="text-sm text-slate-200">Require admin token</div>
|
|
3299
|
+
</div>
|
|
3300
|
+
<button onClick={() => setRequireAuth(!requireAuth)} className={`glass-chip-btn ${requireAuth ? 'is-on' : ''}`}>{requireAuth ? 'On' : 'Off'}</button>
|
|
3301
|
+
</div>
|
|
3302
|
+
</div>
|
|
3303
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
3304
|
+
{([
|
|
3305
|
+
['Endpoint', path],
|
|
3306
|
+
['Introspection', introspection ? 'enabled' : 'disabled'],
|
|
3307
|
+
] as const).map(([label, value]) => (
|
|
3308
|
+
<div key={label} className="glass rounded-xl px-3 py-2">
|
|
3309
|
+
<span className="admin-overline">{label}</span>
|
|
3310
|
+
<code className="block mt-1 truncate font-mono text-sm text-slate-200">{value}</code>
|
|
3311
|
+
</div>
|
|
3312
|
+
))}
|
|
3313
|
+
</div>
|
|
3314
|
+
{requireAuth && <p className="text-xs text-amber-400">When enabled, opening /graphiql requires an active admin session (same guard as /admin/*).</p>}
|
|
3315
|
+
{msg && <p className="text-xs text-slate-400">{msg}</p>}
|
|
3316
|
+
<div className="flex items-center justify-end gap-2">
|
|
3317
|
+
<button onClick={load} className="glass-chip-btn"><Icon name="refresh" size={12} /> Reload</button>
|
|
3318
|
+
<button onClick={save} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : 'Save'} <span>→</span></button>
|
|
3319
|
+
</div>
|
|
3320
|
+
</section>
|
|
3321
|
+
|
|
3322
|
+
<ConfirmDialog
|
|
3323
|
+
open={restartDialog}
|
|
3324
|
+
title="Restart required"
|
|
3325
|
+
message="Your GraphQL settings have been saved. Restart the backend services to apply the new configuration."
|
|
3326
|
+
confirmLabel="Open services"
|
|
3327
|
+
onConfirm={() => { setRestartDialog(false); onNavigate?.('processes'); }}
|
|
3328
|
+
onCancel={() => setRestartDialog(false)}
|
|
3329
|
+
/>
|
|
3330
|
+
</div>
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3232
3334
|
function Users({ currentUserEmail }: { currentUserEmail?: string }) {
|
|
3233
3335
|
const [data, setData] = useState<{ users: UserRecord[] } | null>(null);
|
|
3234
3336
|
const [roles, setRoles] = useState<RoleDefinition[]>([]);
|
|
@@ -68,6 +68,29 @@ export function issueCsrfToken(ctx: RequestContext, options: CsrfOptions = {}):
|
|
|
68
68
|
return issueToken(ctx, options.cookieName ?? 'nexus_csrf', options.cookie ?? { path: '/', sameSite: 'lax', secure: false });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Read the existing double-submit cookie token WITHOUT rotating it. Returns the
|
|
73
|
+
* current `nexus_csrf` value, or undefined when no cookie is present. Use this
|
|
74
|
+
* (not `issueCsrfToken`) when you only need to expose the token, so the cookie
|
|
75
|
+
* stays stable and previously-issued tokens keep matching.
|
|
76
|
+
*/
|
|
77
|
+
export function getCsrfToken(ctx: RequestContext, options: CsrfOptions = {}): string | undefined {
|
|
78
|
+
return readCookie(ctx, options.cookieName ?? 'nexus_csrf');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Verify a mutation's CSRF token against the double-submit cookie. The token may
|
|
83
|
+
* be carried either as an argument (`csrf`) or in the `X-CSRF-Token` header; only
|
|
84
|
+
* one needs to match the cookie (never both). Both channels stay cookie-bound:
|
|
85
|
+
* the provided token must equal the `nexus_csrf` cookie value.
|
|
86
|
+
*/
|
|
87
|
+
export function verifyMutationCsrf(ctx: RequestContext, argToken: string | undefined, headerToken?: string | undefined, options: CsrfOptions = {}): boolean {
|
|
88
|
+
const cookieToken = readCookie(ctx, options.cookieName ?? 'nexus_csrf');
|
|
89
|
+
if (!cookieToken) return false;
|
|
90
|
+
const provided = argToken ?? headerToken;
|
|
91
|
+
return !!provided && timingSafeEqual(cookieToken, provided);
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
/** CSRF + origin check for the WebSocket upgrade request (no CORS preflight). */
|
|
72
95
|
export function checkWsUpgrade(headers: Record<string, string | string[] | undefined>, options: CsrfOptions = {}): void {
|
|
73
96
|
const trusted = options.trustedOrigins ?? [];
|