@useatlas/create 0.0.2 → 0.0.4
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 +4 -18
- package/index.ts +191 -31
- package/package.json +1 -1
- package/templates/docker/.env.example +3 -3
- package/templates/docker/Dockerfile.sidecar +28 -0
- package/templates/docker/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/docker/bin/atlas.ts +108 -44
- package/templates/docker/data/demo-semantic/catalog.yml +51 -27
- package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
- package/templates/docker/data/demo-semantic/glossary.yml +104 -8
- package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/docker/docker-compose.yml +1 -1
- package/templates/docker/docs/deploy.md +2 -39
- package/templates/docker/package.json +17 -1
- package/templates/docker/semantic/catalog.yml +62 -3
- package/templates/docker/semantic/entities/accounts.yml +162 -0
- package/templates/docker/semantic/entities/companies.yml +143 -0
- package/templates/docker/semantic/entities/people.yml +132 -0
- package/templates/docker/semantic/glossary.yml +116 -4
- package/templates/docker/semantic/metrics/accounts.yml +77 -0
- package/templates/docker/semantic/metrics/companies.yml +63 -0
- package/templates/docker/sidecar/Dockerfile +5 -6
- package/templates/docker/sidecar/railway.json +1 -2
- package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/docker/src/api/__tests__/health.test.ts +30 -8
- package/templates/docker/src/api/routes/admin.ts +549 -8
- package/templates/docker/src/api/routes/chat.ts +5 -20
- package/templates/docker/src/api/routes/health.ts +39 -27
- package/templates/docker/src/api/routes/openapi.ts +1329 -74
- package/templates/docker/src/api/routes/query.ts +2 -1
- package/templates/docker/src/api/server.ts +27 -0
- package/templates/docker/src/app/api/[...route]/route.ts +2 -2
- package/templates/docker/src/app/globals.css +13 -12
- package/templates/docker/src/app/layout.tsx +9 -2
- package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/docker/src/components/ui/badge.tsx +48 -0
- package/templates/docker/src/components/ui/button.tsx +64 -0
- package/templates/docker/src/components/ui/card.tsx +92 -0
- package/templates/docker/src/components/ui/collapsible.tsx +33 -0
- package/templates/docker/src/components/ui/command.tsx +184 -0
- package/templates/docker/src/components/ui/dialog.tsx +158 -0
- package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/docker/src/components/ui/input.tsx +21 -0
- package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
- package/templates/docker/src/components/ui/select.tsx +190 -0
- package/templates/docker/src/components/ui/separator.tsx +28 -0
- package/templates/docker/src/components/ui/sheet.tsx +143 -0
- package/templates/docker/src/components/ui/sidebar.tsx +726 -0
- package/templates/docker/src/components/ui/skeleton.tsx +13 -0
- package/templates/docker/src/components/ui/table.tsx +116 -0
- package/templates/docker/src/components/ui/tabs.tsx +91 -0
- package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
- package/templates/docker/src/components/ui/toggle.tsx +47 -0
- package/templates/docker/src/components/ui/tooltip.tsx +57 -0
- package/templates/docker/src/hooks/use-mobile.ts +19 -0
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
- package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/docker/src/lib/agent-query.ts +5 -23
- package/templates/docker/src/lib/agent.ts +32 -112
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/docker/src/lib/auth/middleware.ts +30 -4
- package/templates/docker/src/lib/auth/migrate.ts +97 -0
- package/templates/docker/src/lib/auth/server.ts +12 -1
- package/templates/docker/src/lib/config.ts +37 -39
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/docker/src/lib/db/connection.ts +87 -265
- package/templates/docker/src/lib/db/internal.ts +6 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/docker/src/lib/plugins/index.ts +4 -1
- package/templates/docker/src/lib/plugins/migrate.ts +103 -0
- package/templates/docker/src/lib/plugins/registry.ts +12 -6
- package/templates/docker/src/lib/plugins/wiring.ts +113 -4
- package/templates/docker/src/lib/providers.ts +6 -1
- package/templates/docker/src/lib/security.ts +24 -0
- package/templates/docker/src/lib/semantic.ts +2 -0
- package/templates/docker/src/lib/sidecar-types.ts +12 -1
- package/templates/docker/src/lib/startup.ts +71 -101
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/docker/src/lib/tools/explore.ts +28 -32
- package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/docker/src/lib/tools/python.ts +367 -0
- package/templates/docker/src/lib/tools/registry.ts +49 -22
- package/templates/docker/src/lib/tools/sql.ts +88 -88
- package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/docker/src/ui/context.tsx +1 -1
- package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/docker/tsconfig.json +2 -2
- package/templates/nextjs-standalone/.env.example +1 -1
- package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/nextjs-standalone/bin/atlas.ts +108 -44
- package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
- package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
- package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
- package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/nextjs-standalone/docs/deploy.md +2 -39
- package/templates/nextjs-standalone/package.json +11 -2
- package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
- package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
- package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
- package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
- package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
- package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
- package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
- package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
- package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
- package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
- package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
- package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
- package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
- package/templates/nextjs-standalone/src/api/server.ts +27 -0
- package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
- package/templates/nextjs-standalone/src/app/globals.css +13 -12
- package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
- package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
- package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
- package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
- package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
- package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
- package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
- package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
- package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
- package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
- package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
- package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
- package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
- package/templates/nextjs-standalone/src/lib/config.ts +37 -39
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
- package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
- package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +103 -0
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +12 -6
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
- package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
- package/templates/nextjs-standalone/src/lib/security.ts +24 -0
- package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
- package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
- package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/nextjs-standalone/tsconfig.json +0 -1
- package/templates/nextjs-standalone/vercel.json +4 -1
- package/templates/docker/render.yaml +0 -34
- package/templates/docker/semantic/entities/.gitkeep +0 -0
- package/templates/docker/semantic/metrics/.gitkeep +0 -0
- package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/docker/src/lib/db/duckdb.ts +0 -122
- package/templates/docker/src/lib/db/salesforce.ts +0 -342
- package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/docker/src/lib/tools/salesforce.ts +0 -138
- package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
- package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
- package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
- package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
- package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
import { connections } from "@atlas/api/lib/db/connection";
|
|
21
21
|
import { hasInternalDB, internalQuery } from "@atlas/api/lib/db/internal";
|
|
22
22
|
import { plugins } from "@atlas/api/lib/plugins/registry";
|
|
23
|
+
import { detectAuthMode } from "@atlas/api/lib/auth/detect";
|
|
24
|
+
import type { AtlasRole } from "@atlas/api/lib/auth/types";
|
|
25
|
+
import { ATLAS_ROLES } from "@atlas/api/lib/auth/types";
|
|
23
26
|
|
|
24
27
|
const log = createLogger("admin-routes");
|
|
25
28
|
|
|
@@ -64,13 +67,15 @@ async function adminAuthPreamble(req: Request, requestId: string) {
|
|
|
64
67
|
return { error: { error: "auth_error", message: authResult.error }, status: authResult.status as 401 | 403 | 500 };
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
// Enforce admin role
|
|
68
|
-
|
|
70
|
+
// Enforce admin role — when auth mode is "none" (no auth configured, e.g.
|
|
71
|
+
// local dev), treat the request as an implicit admin since there is no
|
|
72
|
+
// identity boundary to enforce.
|
|
73
|
+
if (authResult.mode !== "none" && (!authResult.user || authResult.user.role !== "admin")) {
|
|
69
74
|
return { error: { error: "forbidden", message: "Admin role required." }, status: 403 as const };
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
const ip = getClientIP(req);
|
|
73
|
-
const rateLimitKey = authResult.user
|
|
78
|
+
const rateLimitKey = authResult.user?.id ?? (ip ? `ip:${ip}` : "anon");
|
|
74
79
|
const rateCheck = checkRateLimit(rateLimitKey);
|
|
75
80
|
if (!rateCheck.allowed) {
|
|
76
81
|
const retryAfterSeconds = Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000);
|
|
@@ -216,8 +221,8 @@ function findEntityFile(root: string, name: string): string | null {
|
|
|
216
221
|
return null;
|
|
217
222
|
}
|
|
218
223
|
|
|
219
|
-
function discoverMetrics(root: string): Array<{ source: string; data: unknown }> {
|
|
220
|
-
const metrics: Array<{ source: string; data: unknown }> = [];
|
|
224
|
+
function discoverMetrics(root: string): Array<{ source: string; file: string; data: unknown }> {
|
|
225
|
+
const metrics: Array<{ source: string; file: string; data: unknown }> = [];
|
|
221
226
|
|
|
222
227
|
const defaultDir = path.join(root, "metrics");
|
|
223
228
|
if (fs.existsSync(defaultDir)) {
|
|
@@ -243,7 +248,7 @@ function discoverMetrics(root: string): Array<{ source: string; data: unknown }>
|
|
|
243
248
|
return metrics;
|
|
244
249
|
}
|
|
245
250
|
|
|
246
|
-
function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: string; data: unknown }>): void {
|
|
251
|
+
function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: string; file: string; data: unknown }>): void {
|
|
247
252
|
let files: string[];
|
|
248
253
|
try {
|
|
249
254
|
files = fs.readdirSync(dir).filter((f) => f.endsWith(".yml"));
|
|
@@ -255,7 +260,7 @@ function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: st
|
|
|
255
260
|
for (const file of files) {
|
|
256
261
|
try {
|
|
257
262
|
const raw = readYamlFile(path.join(dir, file));
|
|
258
|
-
out.push({ source, data: raw });
|
|
263
|
+
out.push({ source, file: file.replace(/\.yml$/, ""), data: raw });
|
|
259
264
|
} catch (err) {
|
|
260
265
|
log.warn({ err: err instanceof Error ? err : new Error(String(err)), file, dir, source }, "Failed to parse metric YAML file");
|
|
261
266
|
}
|
|
@@ -342,7 +347,7 @@ admin.get("/overview", async (c) => {
|
|
|
342
347
|
pluginHealth: pluginList.map((p) => ({
|
|
343
348
|
id: p.id,
|
|
344
349
|
name: p.name,
|
|
345
|
-
|
|
350
|
+
types: p.types,
|
|
346
351
|
status: p.status,
|
|
347
352
|
})),
|
|
348
353
|
});
|
|
@@ -477,6 +482,63 @@ admin.get("/semantic/catalog", async (c) => {
|
|
|
477
482
|
});
|
|
478
483
|
});
|
|
479
484
|
|
|
485
|
+
// GET /semantic/raw/:file — serve raw YAML for top-level files (catalog.yml, glossary.yml)
|
|
486
|
+
// GET /semantic/raw/:dir/:file — serve raw YAML for subdirectory files (entities/x.yml, metrics/x.yml)
|
|
487
|
+
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
489
|
+
function serveRawYaml(c: any, requestId: string, filePath: string) {
|
|
490
|
+
// Validate: no traversal, must be .yml
|
|
491
|
+
if (filePath.includes("..") || filePath.includes("\0") || filePath.includes("\\") || !filePath.endsWith(".yml")) {
|
|
492
|
+
return c.json({ error: "invalid_request", message: "Invalid file path." }, 400);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const allowedPattern = /^(catalog|glossary)\.yml$|^(entities|metrics)\/[a-zA-Z0-9_-]+\.yml$/;
|
|
496
|
+
if (!allowedPattern.test(filePath)) {
|
|
497
|
+
return c.json({ error: "invalid_request", message: "File path not allowed." }, 400);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const root = getSemanticRoot();
|
|
501
|
+
const resolved = path.resolve(root, filePath);
|
|
502
|
+
if (!resolved.startsWith(path.resolve(root))) {
|
|
503
|
+
log.error({ requestId, filePath, resolved, root }, "Raw YAML path escaped semantic root");
|
|
504
|
+
return c.json({ error: "forbidden", message: "Access denied." }, 403);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!fs.existsSync(resolved)) {
|
|
508
|
+
return c.json({ error: "not_found", message: `File "${filePath}" not found.` }, 404);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
513
|
+
return c.text(content);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), filePath }, "Failed to read raw YAML file");
|
|
516
|
+
return c.json({ error: "internal_error", message: "Failed to read file." }, 500);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
admin.get("/semantic/raw/:dir/:file", async (c) => {
|
|
521
|
+
const req = c.req.raw;
|
|
522
|
+
const requestId = crypto.randomUUID();
|
|
523
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
524
|
+
if ("error" in preamble) return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
525
|
+
const { authResult } = preamble;
|
|
526
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
527
|
+
return serveRawYaml(c, requestId, `${c.req.param("dir")}/${c.req.param("file")}`);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
admin.get("/semantic/raw/:file", async (c) => {
|
|
532
|
+
const req = c.req.raw;
|
|
533
|
+
const requestId = crypto.randomUUID();
|
|
534
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
535
|
+
if ("error" in preamble) return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
536
|
+
const { authResult } = preamble;
|
|
537
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
538
|
+
return serveRawYaml(c, requestId, c.req.param("file"));
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
480
542
|
// GET /semantic/stats — aggregate stats
|
|
481
543
|
admin.get("/semantic/stats", async (c) => {
|
|
482
544
|
const req = c.req.raw;
|
|
@@ -754,4 +816,483 @@ admin.post("/plugins/:id/health", async (c) => {
|
|
|
754
816
|
});
|
|
755
817
|
});
|
|
756
818
|
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// Password change required — any authenticated managed-auth user (not admin-only)
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
// GET /me/password-status — check if current user must change password
|
|
824
|
+
admin.get("/me/password-status", async (c) => {
|
|
825
|
+
const req = c.req.raw;
|
|
826
|
+
const requestId = crypto.randomUUID();
|
|
827
|
+
|
|
828
|
+
// Light auth: authenticate but don't require admin role
|
|
829
|
+
let authResult: AuthResult;
|
|
830
|
+
try {
|
|
831
|
+
authResult = await authenticateRequest(req);
|
|
832
|
+
} catch {
|
|
833
|
+
return c.json({ error: "auth_error", message: "Authentication system error" }, 500);
|
|
834
|
+
}
|
|
835
|
+
if (!authResult.authenticated) {
|
|
836
|
+
return c.json({ error: "auth_error", message: authResult.error }, authResult.status);
|
|
837
|
+
}
|
|
838
|
+
if (authResult.mode !== "managed" || !authResult.user) {
|
|
839
|
+
return c.json({ passwordChangeRequired: false });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (!hasInternalDB()) return c.json({ passwordChangeRequired: false });
|
|
843
|
+
|
|
844
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
845
|
+
try {
|
|
846
|
+
const rows = await internalQuery<{ password_change_required: boolean }>(
|
|
847
|
+
`SELECT password_change_required FROM "user" WHERE id = $1`,
|
|
848
|
+
[authResult.user!.id],
|
|
849
|
+
);
|
|
850
|
+
return c.json({ passwordChangeRequired: rows[0]?.password_change_required === true });
|
|
851
|
+
} catch {
|
|
852
|
+
return c.json({ passwordChangeRequired: false });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// POST /me/password — change password and clear the flag
|
|
858
|
+
admin.post("/me/password", async (c) => {
|
|
859
|
+
const req = c.req.raw;
|
|
860
|
+
const requestId = crypto.randomUUID();
|
|
861
|
+
|
|
862
|
+
let authResult: AuthResult;
|
|
863
|
+
try {
|
|
864
|
+
authResult = await authenticateRequest(req);
|
|
865
|
+
} catch {
|
|
866
|
+
return c.json({ error: "auth_error", message: "Authentication system error" }, 500);
|
|
867
|
+
}
|
|
868
|
+
if (!authResult.authenticated) {
|
|
869
|
+
return c.json({ error: "auth_error", message: authResult.error }, authResult.status);
|
|
870
|
+
}
|
|
871
|
+
if (authResult.mode !== "managed" || !authResult.user) {
|
|
872
|
+
return c.json({ error: "not_available", message: "Password change requires managed auth mode." }, 404);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
876
|
+
const body = await c.req.json().catch((err) => {
|
|
877
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in password change request");
|
|
878
|
+
return null;
|
|
879
|
+
});
|
|
880
|
+
const currentPassword = body?.currentPassword;
|
|
881
|
+
const newPassword = body?.newPassword;
|
|
882
|
+
|
|
883
|
+
if (typeof currentPassword !== "string" || typeof newPassword !== "string") {
|
|
884
|
+
return c.json({ error: "invalid_request", message: "currentPassword and newPassword are required." }, 400);
|
|
885
|
+
}
|
|
886
|
+
if (newPassword.length < 8) {
|
|
887
|
+
return c.json({ error: "invalid_request", message: "New password must be at least 8 characters." }, 400);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const { getAuthInstance } = await import("@atlas/api/lib/auth/server");
|
|
892
|
+
const auth = getAuthInstance();
|
|
893
|
+
await (auth.api as unknown as {
|
|
894
|
+
changePassword(opts: { body: { currentPassword: string; newPassword: string }; headers: Headers }): Promise<unknown>;
|
|
895
|
+
}).changePassword({
|
|
896
|
+
body: { currentPassword, newPassword },
|
|
897
|
+
headers: req.headers,
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Clear the flag
|
|
901
|
+
if (hasInternalDB()) {
|
|
902
|
+
await internalQuery(
|
|
903
|
+
`UPDATE "user" SET password_change_required = false WHERE id = $1`,
|
|
904
|
+
[authResult.user!.id],
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
log.info({ requestId, userId: authResult.user!.id }, "Password changed and flag cleared");
|
|
909
|
+
return c.json({ success: true });
|
|
910
|
+
} catch (err) {
|
|
911
|
+
const message = err instanceof Error ? err.message : "Password change failed";
|
|
912
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Password change failed");
|
|
913
|
+
// Better Auth throws if current password is wrong
|
|
914
|
+
if (message.includes("password") || message.includes("incorrect") || message.includes("invalid")) {
|
|
915
|
+
return c.json({ error: "invalid_request", message: "Current password is incorrect." }, 400);
|
|
916
|
+
}
|
|
917
|
+
return c.json({ error: "internal_error", message: "Failed to change password." }, 500);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ---------------------------------------------------------------------------
|
|
923
|
+
// User management routes (requires managed auth mode + Better Auth admin plugin)
|
|
924
|
+
// ---------------------------------------------------------------------------
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Server-side admin API methods from Better Auth's admin plugin.
|
|
928
|
+
* The base Auth type doesn't expose plugin-specific methods (see server.ts
|
|
929
|
+
* for why), but they exist at runtime. This interface types the subset we use.
|
|
930
|
+
*/
|
|
931
|
+
interface AdminApi {
|
|
932
|
+
listUsers(opts: { query: Record<string, unknown>; headers: Headers }): Promise<{
|
|
933
|
+
users: Array<Record<string, unknown>>;
|
|
934
|
+
total: number;
|
|
935
|
+
}>;
|
|
936
|
+
setRole(opts: { body: { userId: string; role: string }; headers: Headers }): Promise<unknown>;
|
|
937
|
+
banUser(opts: { body: Record<string, unknown>; headers: Headers }): Promise<unknown>;
|
|
938
|
+
unbanUser(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
|
|
939
|
+
removeUser(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
|
|
940
|
+
revokeSessions(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Get the Better Auth instance's admin API, or null if managed auth is not active.
|
|
945
|
+
* Lazy-imports to avoid pulling in Better Auth when not needed.
|
|
946
|
+
*/
|
|
947
|
+
async function getAdminApi(): Promise<AdminApi | null> {
|
|
948
|
+
if (detectAuthMode() !== "managed") return null;
|
|
949
|
+
const { getAuthInstance } = await import("@atlas/api/lib/auth/server");
|
|
950
|
+
// Cast: admin plugin methods exist at runtime but aren't in the base Auth type
|
|
951
|
+
return getAuthInstance().api as unknown as AdminApi;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Validate that a role string is a valid Atlas role. */
|
|
955
|
+
function isValidRole(role: unknown): role is AtlasRole {
|
|
956
|
+
return typeof role === "string" && (ATLAS_ROLES as readonly string[]).includes(role);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// GET /users — list users (paginated, filterable)
|
|
960
|
+
admin.get("/users", async (c) => {
|
|
961
|
+
const req = c.req.raw;
|
|
962
|
+
const requestId = crypto.randomUUID();
|
|
963
|
+
|
|
964
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
965
|
+
if ("error" in preamble) {
|
|
966
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
967
|
+
}
|
|
968
|
+
const { authResult } = preamble;
|
|
969
|
+
|
|
970
|
+
const adminApi = await getAdminApi();
|
|
971
|
+
if (!adminApi) {
|
|
972
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
976
|
+
const rawLimit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
977
|
+
const rawOffset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
978
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 200) : 50;
|
|
979
|
+
const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
|
|
980
|
+
const search = c.req.query("search");
|
|
981
|
+
const role = c.req.query("role");
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
const result = await adminApi.listUsers({
|
|
985
|
+
query: {
|
|
986
|
+
limit,
|
|
987
|
+
offset,
|
|
988
|
+
...(search ? { searchField: "email", searchValue: search, searchOperator: "contains" } : {}),
|
|
989
|
+
...(role && isValidRole(role) ? { filterField: "role", filterValue: role, filterOperator: "eq" } : {}),
|
|
990
|
+
sortBy: "createdAt",
|
|
991
|
+
sortDirection: "desc",
|
|
992
|
+
},
|
|
993
|
+
headers: req.headers,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
return c.json({
|
|
997
|
+
users: result.users.map((u: Record<string, unknown>) => ({
|
|
998
|
+
id: u.id,
|
|
999
|
+
email: u.email,
|
|
1000
|
+
name: u.name,
|
|
1001
|
+
role: u.role ?? "viewer",
|
|
1002
|
+
banned: u.banned ?? false,
|
|
1003
|
+
banReason: u.banReason ?? null,
|
|
1004
|
+
banExpires: u.banExpires ?? null,
|
|
1005
|
+
createdAt: u.createdAt,
|
|
1006
|
+
})),
|
|
1007
|
+
total: result.total,
|
|
1008
|
+
limit,
|
|
1009
|
+
offset,
|
|
1010
|
+
});
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Failed to list users");
|
|
1013
|
+
return c.json({ error: "internal_error", message: "Failed to list users." }, 500);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// GET /users/stats — aggregate user stats
|
|
1019
|
+
admin.get("/users/stats", async (c) => {
|
|
1020
|
+
const req = c.req.raw;
|
|
1021
|
+
const requestId = crypto.randomUUID();
|
|
1022
|
+
|
|
1023
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1024
|
+
if ("error" in preamble) {
|
|
1025
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1026
|
+
}
|
|
1027
|
+
const { authResult } = preamble;
|
|
1028
|
+
|
|
1029
|
+
if (!hasInternalDB() || detectAuthMode() !== "managed") {
|
|
1030
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1034
|
+
try {
|
|
1035
|
+
const totalResult = await internalQuery<{ count: string }>(
|
|
1036
|
+
`SELECT COUNT(*) as count FROM "user"`,
|
|
1037
|
+
);
|
|
1038
|
+
const roleResult = await internalQuery<{ role: string; count: string }>(
|
|
1039
|
+
`SELECT COALESCE(role, 'viewer') as role, COUNT(*) as count FROM "user" GROUP BY COALESCE(role, 'viewer')`,
|
|
1040
|
+
);
|
|
1041
|
+
const bannedResult = await internalQuery<{ count: string }>(
|
|
1042
|
+
`SELECT COUNT(*) as count FROM "user" WHERE banned = true`,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
const total = parseInt(String(totalResult[0]?.count ?? "0"), 10);
|
|
1046
|
+
const banned = parseInt(String(bannedResult[0]?.count ?? "0"), 10);
|
|
1047
|
+
const byRole: Record<string, number> = {};
|
|
1048
|
+
for (const r of roleResult) {
|
|
1049
|
+
byRole[r.role] = parseInt(String(r.count), 10);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return c.json({ total, banned, byRole });
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "User stats query failed");
|
|
1055
|
+
return c.json({ error: "internal_error", message: "Failed to query user stats." }, 500);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// PATCH /users/:id/role — change user role
|
|
1061
|
+
admin.patch("/users/:id/role", async (c) => {
|
|
1062
|
+
const req = c.req.raw;
|
|
1063
|
+
const requestId = crypto.randomUUID();
|
|
1064
|
+
|
|
1065
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1066
|
+
if ("error" in preamble) {
|
|
1067
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1068
|
+
}
|
|
1069
|
+
const { authResult } = preamble;
|
|
1070
|
+
|
|
1071
|
+
const adminApi = await getAdminApi();
|
|
1072
|
+
if (!adminApi) {
|
|
1073
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1077
|
+
const userId = c.req.param("id");
|
|
1078
|
+
const body = await c.req.json().catch((err) => {
|
|
1079
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in role change request");
|
|
1080
|
+
return null;
|
|
1081
|
+
});
|
|
1082
|
+
const newRole = body?.role;
|
|
1083
|
+
|
|
1084
|
+
if (!isValidRole(newRole)) {
|
|
1085
|
+
return c.json({ error: "invalid_request", message: `Invalid role. Must be one of: ${ATLAS_ROLES.join(", ")}` }, 400);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Self-protection: cannot change own role
|
|
1089
|
+
if (authResult.user?.id === userId) {
|
|
1090
|
+
return c.json({ error: "forbidden", message: "Cannot change your own role." }, 403);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Last admin guard: if demoting an admin, ensure at least one admin remains
|
|
1094
|
+
if (newRole !== "admin" && hasInternalDB()) {
|
|
1095
|
+
try {
|
|
1096
|
+
const currentUser = await internalQuery<{ role: string }>(
|
|
1097
|
+
`SELECT role FROM "user" WHERE id = $1`,
|
|
1098
|
+
[userId],
|
|
1099
|
+
);
|
|
1100
|
+
if (currentUser[0]?.role === "admin") {
|
|
1101
|
+
const adminCount = await internalQuery<{ count: string }>(
|
|
1102
|
+
`SELECT COUNT(*) as count FROM "user" WHERE role = 'admin'`,
|
|
1103
|
+
);
|
|
1104
|
+
if (parseInt(String(adminCount[0]?.count ?? "0"), 10) <= 1) {
|
|
1105
|
+
return c.json({ error: "forbidden", message: "Cannot demote the last admin." }, 403);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Last admin guard check failed");
|
|
1110
|
+
return c.json({ error: "internal_error", message: "Failed to verify admin count." }, 500);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
await adminApi.setRole({
|
|
1116
|
+
body: { userId, role: newRole },
|
|
1117
|
+
headers: req.headers,
|
|
1118
|
+
});
|
|
1119
|
+
log.info({ requestId, targetUserId: userId, newRole, actorId: authResult.user?.id }, "User role changed");
|
|
1120
|
+
return c.json({ success: true });
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to set user role");
|
|
1123
|
+
return c.json({ error: "internal_error", message: "Failed to update user role." }, 500);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
// POST /users/:id/ban — ban a user
|
|
1129
|
+
admin.post("/users/:id/ban", async (c) => {
|
|
1130
|
+
const req = c.req.raw;
|
|
1131
|
+
const requestId = crypto.randomUUID();
|
|
1132
|
+
|
|
1133
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1134
|
+
if ("error" in preamble) {
|
|
1135
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1136
|
+
}
|
|
1137
|
+
const { authResult } = preamble;
|
|
1138
|
+
|
|
1139
|
+
const adminApi = await getAdminApi();
|
|
1140
|
+
if (!adminApi) {
|
|
1141
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1145
|
+
const userId = c.req.param("id");
|
|
1146
|
+
|
|
1147
|
+
if (authResult.user?.id === userId) {
|
|
1148
|
+
return c.json({ error: "forbidden", message: "Cannot ban yourself." }, 403);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const body = await c.req.json().catch((err) => {
|
|
1152
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in ban user request");
|
|
1153
|
+
return {};
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
await adminApi.banUser({
|
|
1158
|
+
body: {
|
|
1159
|
+
userId,
|
|
1160
|
+
...(body.reason ? { banReason: body.reason } : {}),
|
|
1161
|
+
...(body.expiresIn ? { banExpiresIn: body.expiresIn } : {}),
|
|
1162
|
+
},
|
|
1163
|
+
headers: req.headers,
|
|
1164
|
+
});
|
|
1165
|
+
log.info({ requestId, targetUserId: userId, reason: body.reason, actorId: authResult.user?.id }, "User banned");
|
|
1166
|
+
return c.json({ success: true });
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to ban user");
|
|
1169
|
+
return c.json({ error: "internal_error", message: "Failed to ban user." }, 500);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// POST /users/:id/unban — unban a user
|
|
1175
|
+
admin.post("/users/:id/unban", async (c) => {
|
|
1176
|
+
const req = c.req.raw;
|
|
1177
|
+
const requestId = crypto.randomUUID();
|
|
1178
|
+
|
|
1179
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1180
|
+
if ("error" in preamble) {
|
|
1181
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1182
|
+
}
|
|
1183
|
+
const { authResult } = preamble;
|
|
1184
|
+
|
|
1185
|
+
const adminApi = await getAdminApi();
|
|
1186
|
+
if (!adminApi) {
|
|
1187
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1191
|
+
const userId = c.req.param("id");
|
|
1192
|
+
|
|
1193
|
+
try {
|
|
1194
|
+
await adminApi.unbanUser({
|
|
1195
|
+
body: { userId },
|
|
1196
|
+
headers: req.headers,
|
|
1197
|
+
});
|
|
1198
|
+
log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User unbanned");
|
|
1199
|
+
return c.json({ success: true });
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to unban user");
|
|
1202
|
+
return c.json({ error: "internal_error", message: "Failed to unban user." }, 500);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// DELETE /users/:id — delete a user
|
|
1208
|
+
admin.delete("/users/:id", async (c) => {
|
|
1209
|
+
const req = c.req.raw;
|
|
1210
|
+
const requestId = crypto.randomUUID();
|
|
1211
|
+
|
|
1212
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1213
|
+
if ("error" in preamble) {
|
|
1214
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1215
|
+
}
|
|
1216
|
+
const { authResult } = preamble;
|
|
1217
|
+
|
|
1218
|
+
const adminApi = await getAdminApi();
|
|
1219
|
+
if (!adminApi) {
|
|
1220
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1224
|
+
const userId = c.req.param("id");
|
|
1225
|
+
|
|
1226
|
+
if (authResult.user?.id === userId) {
|
|
1227
|
+
return c.json({ error: "forbidden", message: "Cannot delete yourself." }, 403);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Last admin guard
|
|
1231
|
+
if (hasInternalDB()) {
|
|
1232
|
+
try {
|
|
1233
|
+
const currentUser = await internalQuery<{ role: string }>(
|
|
1234
|
+
`SELECT role FROM "user" WHERE id = $1`,
|
|
1235
|
+
[userId],
|
|
1236
|
+
);
|
|
1237
|
+
if (currentUser[0]?.role === "admin") {
|
|
1238
|
+
const adminCount = await internalQuery<{ count: string }>(
|
|
1239
|
+
`SELECT COUNT(*) as count FROM "user" WHERE role = 'admin'`,
|
|
1240
|
+
);
|
|
1241
|
+
if (parseInt(String(adminCount[0]?.count ?? "0"), 10) <= 1) {
|
|
1242
|
+
return c.json({ error: "forbidden", message: "Cannot delete the last admin." }, 403);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Last admin guard check failed");
|
|
1247
|
+
return c.json({ error: "internal_error", message: "Failed to verify admin count." }, 500);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
await adminApi.removeUser({
|
|
1253
|
+
body: { userId },
|
|
1254
|
+
headers: req.headers,
|
|
1255
|
+
});
|
|
1256
|
+
log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User deleted");
|
|
1257
|
+
return c.json({ success: true });
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to delete user");
|
|
1260
|
+
return c.json({ error: "internal_error", message: "Failed to delete user." }, 500);
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// POST /users/:id/revoke — revoke all sessions (force logout)
|
|
1266
|
+
admin.post("/users/:id/revoke", async (c) => {
|
|
1267
|
+
const req = c.req.raw;
|
|
1268
|
+
const requestId = crypto.randomUUID();
|
|
1269
|
+
|
|
1270
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
1271
|
+
if ("error" in preamble) {
|
|
1272
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
1273
|
+
}
|
|
1274
|
+
const { authResult } = preamble;
|
|
1275
|
+
|
|
1276
|
+
const adminApi = await getAdminApi();
|
|
1277
|
+
if (!adminApi) {
|
|
1278
|
+
return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
1282
|
+
const userId = c.req.param("id");
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
await adminApi.revokeSessions({
|
|
1286
|
+
body: { userId },
|
|
1287
|
+
headers: req.headers,
|
|
1288
|
+
});
|
|
1289
|
+
log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User sessions revoked");
|
|
1290
|
+
return c.json({ success: true });
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to revoke sessions");
|
|
1293
|
+
return c.json({ error: "internal_error", message: "Failed to revoke sessions." }, 500);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
757
1298
|
export { admin };
|
|
@@ -118,7 +118,8 @@ chat.post("/", async (c) => {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// Datasource guard — diagnostics pass (it's a warning) but chat requires a datasource
|
|
121
|
-
|
|
121
|
+
const { resolveDatasourceUrl } = await import("@atlas/api/lib/db/connection");
|
|
122
|
+
if (!resolveDatasourceUrl()) {
|
|
122
123
|
return c.json(
|
|
123
124
|
{
|
|
124
125
|
error: "no_datasource",
|
|
@@ -204,29 +205,13 @@ chat.post("/", async (c) => {
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
try {
|
|
207
|
-
// Build a dynamic registry when
|
|
208
|
+
// Build a dynamic registry when actions are enabled
|
|
208
209
|
let toolRegistry;
|
|
209
210
|
const includeActions = process.env.ATLAS_ACTIONS_ENABLED === "true";
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const { listSalesforceSources } = await import("@atlas/api/lib/db/salesforce");
|
|
213
|
-
includeSalesforce = listSalesforceSources().length > 0;
|
|
214
|
-
} catch (err) {
|
|
215
|
-
const isModuleNotFound =
|
|
216
|
-
err instanceof Error &&
|
|
217
|
-
(err.message.includes("Cannot find module") ||
|
|
218
|
-
err.message.includes("MODULE_NOT_FOUND"));
|
|
219
|
-
if (!isModuleNotFound) {
|
|
220
|
-
log.error(
|
|
221
|
-
{ err: err instanceof Error ? err : new Error(String(err)) },
|
|
222
|
-
"Failed to initialize Salesforce tool registry — falling back to default tools",
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (includeSalesforce || includeActions) {
|
|
211
|
+
if (includeActions) {
|
|
227
212
|
try {
|
|
228
213
|
const { buildRegistry } = await import("@atlas/api/lib/tools/registry");
|
|
229
|
-
toolRegistry = await buildRegistry({
|
|
214
|
+
toolRegistry = await buildRegistry({ includeActions });
|
|
230
215
|
} catch (err) {
|
|
231
216
|
log.error(
|
|
232
217
|
{ err: err instanceof Error ? err : new Error(String(err)) },
|