@bevel-software/platform-core-backend 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/core-ports.d.ts +11 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +30 -11
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +112 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +176 -7
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +69 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +153 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +111 -4
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +255 -25
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +309 -115
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +32 -14
- package/src/core/create-core-services.ts +129 -23
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +179 -6
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/git.service.ts +168 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.service.ts +121 -4
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +256 -24
- package/src/modules/workspace/workspace.tools.ts +338 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +31 -0
- package/src/shared/git.contract.ts +27 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -3,7 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import type { Server } from 'node:http';
|
|
4
4
|
import { createAccountRoutes } from '../account.routes.js';
|
|
5
5
|
import { AuthService } from '../auth.service.js';
|
|
6
|
-
import { AccountAdmissionRefusedError } from '../account-admission.js';
|
|
6
|
+
import { AccountAdmissionRefusedError, AccountChangeRefusedError } from '../account-admission.js';
|
|
7
7
|
import { hashPassword } from '../password-hash.js';
|
|
8
8
|
import type { Database } from '../../database/connection.js';
|
|
9
9
|
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
@@ -13,6 +13,8 @@ const authService = {
|
|
|
13
13
|
{ id: 'u1', email: 'a@example.com', name: 'A', hasPassword: true, createdAt: new Date() },
|
|
14
14
|
]),
|
|
15
15
|
createAccount: vi.fn(async (email: string) => ({ id: 'u2', email, name: 'B' })),
|
|
16
|
+
deactivate: vi.fn(async (userId: string) => userId !== 'missing'),
|
|
17
|
+
reactivate: vi.fn(async (userId: string) => userId !== 'missing'),
|
|
16
18
|
} as unknown as AuthService;
|
|
17
19
|
|
|
18
20
|
// Satisfies the route's narrow Pick<IAccountErasureService, 'eraseUser'>
|
|
@@ -172,12 +174,12 @@ describe('account routes — admin gate', () => {
|
|
|
172
174
|
expect(JSON.stringify(body)).not.toContain('relation');
|
|
173
175
|
});
|
|
174
176
|
|
|
175
|
-
it('400s
|
|
177
|
+
it('400s without an email (a password is optional) and surfaces service validation errors', async () => {
|
|
176
178
|
const base = await listen(makeApp({ admin: true }));
|
|
177
179
|
const missing = await fetch(`${base}/api/admin/accounts`, {
|
|
178
180
|
method: 'POST',
|
|
179
181
|
headers: { 'Content-Type': 'application/json' },
|
|
180
|
-
body: JSON.stringify({
|
|
182
|
+
body: JSON.stringify({ name: 'B' }),
|
|
181
183
|
});
|
|
182
184
|
expect(missing.status).toBe(400);
|
|
183
185
|
|
|
@@ -326,3 +328,79 @@ describe('account routes — removing the erased address from access files', ()
|
|
|
326
328
|
expect(accessRemoval.remove).not.toHaveBeenCalled();
|
|
327
329
|
});
|
|
328
330
|
});
|
|
331
|
+
|
|
332
|
+
describe('account routes — switching an account off and on', () => {
|
|
333
|
+
const post = (base: string, path: string) => fetch(`${base}/api/admin/accounts/${path}`, { method: 'POST' });
|
|
334
|
+
|
|
335
|
+
it('switches another account off and back on', async () => {
|
|
336
|
+
const base = await listen(makeApp({ admin: true }));
|
|
337
|
+
expect((await post(base, 'u2/deactivate')).status).toBe(204);
|
|
338
|
+
expect(authService.deactivate).toHaveBeenCalledWith('u2');
|
|
339
|
+
expect((await post(base, 'u2/reactivate')).status).toBe(204);
|
|
340
|
+
expect(authService.reactivate).toHaveBeenCalledWith('u2');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('refuses an admin switching off their own account, so an admin who can sign in always remains', async () => {
|
|
344
|
+
vi.mocked(authService.deactivate).mockClear();
|
|
345
|
+
const base = await listen(makeApp({ admin: true }));
|
|
346
|
+
const res = await post(base, 'u1/deactivate');
|
|
347
|
+
expect(res.status).toBe(400);
|
|
348
|
+
expect(authService.deactivate).not.toHaveBeenCalled();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('refuses non-admins', async () => {
|
|
352
|
+
vi.mocked(authService.deactivate).mockClear();
|
|
353
|
+
const base = await listen(makeApp({ admin: false }));
|
|
354
|
+
expect((await post(base, 'u2/deactivate')).status).toBe(403);
|
|
355
|
+
expect((await post(base, 'u2/reactivate')).status).toBe(403);
|
|
356
|
+
expect(authService.deactivate).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('404s for an account that does not exist', async () => {
|
|
360
|
+
const base = await listen(makeApp({ admin: true }));
|
|
361
|
+
expect((await post(base, 'missing/deactivate')).status).toBe(404);
|
|
362
|
+
expect((await post(base, 'missing/reactivate')).status).toBe(404);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("answers a reactivation the deployment has no room for with 403 and the port's words", async () => {
|
|
366
|
+
vi.mocked(authService.reactivate).mockRejectedValueOnce(new AccountAdmissionRefusedError('All 3 seats are taken'));
|
|
367
|
+
const base = await listen(makeApp({ admin: true }));
|
|
368
|
+
const res = await post(base, 'u2/reactivate');
|
|
369
|
+
expect(res.status).toBe(403);
|
|
370
|
+
expect(((await res.json()) as { error: string }).error).toBe('All 3 seats are taken');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('answers a refused deactivation (the deployment admin, say) with 400 and the reason', async () => {
|
|
374
|
+
vi.mocked(authService.deactivate).mockRejectedValueOnce(new AccountChangeRefusedError('The deployment admin cannot be switched off'));
|
|
375
|
+
const base = await listen(makeApp({ admin: true }));
|
|
376
|
+
const res = await post(base, 'u2/deactivate');
|
|
377
|
+
expect(res.status).toBe(400);
|
|
378
|
+
expect(((await res.json()) as { error: string }).error).toContain('deployment admin');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('answers an unexpected failure with a 500 that names nothing of it', async () => {
|
|
382
|
+
vi.mocked(authService.deactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
|
|
383
|
+
vi.mocked(authService.reactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
|
|
384
|
+
const base = await listen(makeApp({ admin: true }));
|
|
385
|
+
const off = await post(base, 'u2/deactivate');
|
|
386
|
+
expect(off.status).toBe(500);
|
|
387
|
+
expect(JSON.stringify(await off.json())).not.toContain('relation');
|
|
388
|
+
const on = await post(base, 'u2/reactivate');
|
|
389
|
+
expect(on.status).toBe(500);
|
|
390
|
+
expect(JSON.stringify(await on.json())).not.toContain('relation');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('refuses a password that is not a string, rather than making an account for single sign-on', async () => {
|
|
394
|
+
vi.mocked(authService.createAccount).mockClear();
|
|
395
|
+
const base = await listen(makeApp({ admin: true }));
|
|
396
|
+
for (const password of [false, null, 0]) {
|
|
397
|
+
const res = await fetch(`${base}/api/admin/accounts`, {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
400
|
+
body: JSON.stringify({ email: 'b@example.com', password }),
|
|
401
|
+
});
|
|
402
|
+
expect(res.status).toBe(400);
|
|
403
|
+
}
|
|
404
|
+
expect(authService.createAccount).not.toHaveBeenCalled();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import type { Request, Response, NextFunction } from 'express';
|
|
3
3
|
import { createAuthMiddleware, AUTH_COOKIE_NAME } from '../auth.middleware.js';
|
|
4
|
+
import { AccountDeactivatedError, AuthBackendError } from '../account-admission.js';
|
|
4
5
|
import type { AuthService } from '../auth.service.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -11,68 +12,90 @@ import type { AuthService } from '../auth.service.js';
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const GOOD_TOKEN = 'good-token';
|
|
15
|
+
const OFF_TOKEN = 'switched-off-token';
|
|
16
|
+
const DOWN_TOKEN = 'database-down-token';
|
|
14
17
|
const IDENTITY = { userId: 'user-1', email: 'alice@example.com' };
|
|
15
18
|
|
|
16
|
-
function run(headers: Record<string, string>) {
|
|
17
|
-
const
|
|
19
|
+
async function run(headers: Record<string, string>) {
|
|
20
|
+
const resolveSession = vi.fn(async (token: string) => {
|
|
21
|
+
if (token === OFF_TOKEN) throw new AccountDeactivatedError();
|
|
22
|
+
if (token === DOWN_TOKEN) throw new AuthBackendError(new Error('db down'));
|
|
18
23
|
if (token !== GOOD_TOKEN) throw new Error('bad token');
|
|
19
24
|
return IDENTITY;
|
|
20
25
|
});
|
|
21
|
-
const authService = {
|
|
26
|
+
const authService = { resolveSession } as unknown as AuthService;
|
|
22
27
|
const req = { headers } as unknown as Request;
|
|
23
28
|
const json = vi.fn();
|
|
24
29
|
const status = vi.fn(() => ({ json }));
|
|
25
30
|
const res = { status } as unknown as Response;
|
|
26
31
|
const next = vi.fn() as unknown as NextFunction;
|
|
27
|
-
createAuthMiddleware(authService)(req, res, next);
|
|
28
|
-
return { req, next, status, json,
|
|
32
|
+
await createAuthMiddleware(authService)(req, res, next);
|
|
33
|
+
return { req, next, status, json, resolveSession };
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
describe('createAuthMiddleware', () => {
|
|
32
|
-
it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', () => {
|
|
33
|
-
const { req, next, status } = run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
|
|
36
|
+
describe('createAuthMiddleware', async () => {
|
|
37
|
+
it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', async () => {
|
|
38
|
+
const { req, next, status } = await run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
|
|
34
39
|
expect(next).toHaveBeenCalledTimes(1);
|
|
35
40
|
expect(status).not.toHaveBeenCalled();
|
|
36
41
|
expect(req.userId).toBe(IDENTITY.userId);
|
|
37
42
|
expect(req.userEmail).toBe(IDENTITY.email);
|
|
38
43
|
});
|
|
39
44
|
|
|
40
|
-
it('finds the cookie among others, and decodes a percent-encoded value', () => {
|
|
41
|
-
const { next,
|
|
45
|
+
it('finds the cookie among others, and decodes a percent-encoded value', async () => {
|
|
46
|
+
const { next, resolveSession } = await run({
|
|
42
47
|
cookie: `theme=dark; ${AUTH_COOKIE_NAME}=${encodeURIComponent(GOOD_TOKEN)}; seen=1`,
|
|
43
48
|
});
|
|
44
|
-
expect(
|
|
49
|
+
expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
|
|
45
50
|
expect(next).toHaveBeenCalledTimes(1);
|
|
46
51
|
});
|
|
47
52
|
|
|
48
|
-
it('prefers the Bearer header when both are present', () => {
|
|
49
|
-
const { next,
|
|
53
|
+
it('prefers the Bearer header when both are present', async () => {
|
|
54
|
+
const { next, resolveSession } = await run({
|
|
50
55
|
authorization: `Bearer ${GOOD_TOKEN}`,
|
|
51
56
|
cookie: `${AUTH_COOKIE_NAME}=stale-cookie`,
|
|
52
57
|
});
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
58
|
+
expect(resolveSession).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
|
|
55
60
|
expect(next).toHaveBeenCalledTimes(1);
|
|
56
61
|
});
|
|
57
62
|
|
|
58
|
-
it('refuses a request with neither', () => {
|
|
59
|
-
const { next, status, json } = run({});
|
|
63
|
+
it('refuses a request with neither', async () => {
|
|
64
|
+
const { next, status, json } = await run({});
|
|
60
65
|
expect(next).not.toHaveBeenCalled();
|
|
61
66
|
expect(status).toHaveBeenCalledWith(401);
|
|
62
67
|
expect(json.mock.calls[0][0].error).toContain('auth cookie');
|
|
63
68
|
});
|
|
64
69
|
|
|
65
|
-
it('refuses an empty cookie value as missing, not as a token to verify', () => {
|
|
66
|
-
const { next, status,
|
|
67
|
-
expect(
|
|
70
|
+
it('refuses an empty cookie value as missing, not as a token to verify', async () => {
|
|
71
|
+
const { next, status, resolveSession } = await run({ cookie: `${AUTH_COOKIE_NAME}=` });
|
|
72
|
+
expect(resolveSession).not.toHaveBeenCalled();
|
|
68
73
|
expect(next).not.toHaveBeenCalled();
|
|
69
74
|
expect(status).toHaveBeenCalledWith(401);
|
|
70
75
|
});
|
|
71
76
|
|
|
72
|
-
it('refuses a cookie the auth service rejects', () => {
|
|
73
|
-
const { next, status, json } = run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
|
|
77
|
+
it('refuses a cookie the auth service rejects', async () => {
|
|
78
|
+
const { next, status, json } = await run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
|
|
74
79
|
expect(next).not.toHaveBeenCalled();
|
|
75
80
|
expect(status).toHaveBeenCalledWith(401);
|
|
76
81
|
expect(json.mock.calls[0][0].error).toBe('Invalid or expired token');
|
|
77
82
|
});
|
|
78
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('createAuthMiddleware — a switched-off account', () => {
|
|
86
|
+
it('refuses a valid session whose account an admin switched off, and says why', async () => {
|
|
87
|
+
const { next, status, json } = await run({ authorization: `Bearer ${OFF_TOKEN}` });
|
|
88
|
+
expect(next).not.toHaveBeenCalled();
|
|
89
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
90
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('createAuthMiddleware — the account cannot be looked up', () => {
|
|
95
|
+
it('500s rather than signing a valid session out during an outage', async () => {
|
|
96
|
+
const { next, status, json } = await run({ authorization: `Bearer ${DOWN_TOKEN}` });
|
|
97
|
+
expect(next).not.toHaveBeenCalled();
|
|
98
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
99
|
+
expect(json.mock.calls[0][0].error).toBe('Authentication backend unavailable');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -537,7 +537,7 @@ describe('AuthService.listAccounts', () => {
|
|
|
537
537
|
expect(json).not.toContain('sup3r-secret');
|
|
538
538
|
for (const account of [...withHash, ...withoutHash]) {
|
|
539
539
|
expect(Object.keys(account).sort()).toEqual(
|
|
540
|
-
['createdAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'name'],
|
|
540
|
+
['createdAt', 'deactivatedAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'isSystem', 'name'],
|
|
541
541
|
);
|
|
542
542
|
}
|
|
543
543
|
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Whether a NEW account may be provisioned on this deployment, asked before
|
|
3
|
-
* the row is inserted
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the row is inserted — and whether a deactivated one may be switched back
|
|
4
|
+
* on, which takes up the same place. The one seam a host that sells seats
|
|
5
|
+
* needs: core admits everyone, and a cloud overlay answers from its plan
|
|
6
|
+
* without forking any sign-in path.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* is
|
|
9
|
-
* already in
|
|
8
|
+
* Never asked for an account that is already on. An existing user's sign-in
|
|
9
|
+
* is not a provisioning, so a refusal can never lock out someone who is
|
|
10
|
+
* already in; taking someone out is the admin's act (deactivation), not the
|
|
11
|
+
* port's.
|
|
10
12
|
*/
|
|
11
13
|
export type AccountProvisionReason =
|
|
12
14
|
/** A first sign-in through single sign-on, which provisions on its own. */
|
|
@@ -16,9 +18,29 @@ export type AccountProvisionReason =
|
|
|
16
18
|
/** The deployment owner's first password sign-in, which creates their row. */
|
|
17
19
|
| 'bootstrap'
|
|
18
20
|
/** A verified identity from an embedding surface that needs a row without a session. */
|
|
19
|
-
| 'embed'
|
|
21
|
+
| 'embed'
|
|
22
|
+
/** An admin switching a deactivated account back on. */
|
|
23
|
+
| 'reactivate';
|
|
20
24
|
|
|
21
|
-
export type AccountAdmissionVerdict =
|
|
25
|
+
export type AccountAdmissionVerdict =
|
|
26
|
+
| { ok: true }
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
message: string;
|
|
30
|
+
/**
|
|
31
|
+
* Keep the person on file instead of turning them away: the account is
|
|
32
|
+
* created DEACTIVATED, the sign-in is still refused with `message`, and
|
|
33
|
+
* an admin finds them on the accounts list, ready to be switched on.
|
|
34
|
+
* For a host whose rule is "whoever is on our domain may join while
|
|
35
|
+
* there is room" — the one past the room waits instead of bouncing.
|
|
36
|
+
*
|
|
37
|
+
* Honoured only for a sign-in through single sign-on, the one
|
|
38
|
+
* provisioning nobody asked for. Everywhere else a verdict carrying it
|
|
39
|
+
* is a plain refusal: an admin creating an account would be told no,
|
|
40
|
+
* and a deactivated account is never reactivated into one.
|
|
41
|
+
*/
|
|
42
|
+
waitForAdmin?: boolean;
|
|
43
|
+
};
|
|
22
44
|
|
|
23
45
|
export interface IAccountAdmission {
|
|
24
46
|
canProvision(email: string, reason: AccountProvisionReason): Promise<AccountAdmissionVerdict>;
|
|
@@ -36,9 +58,56 @@ export const admitEveryone: IAccountAdmission = {
|
|
|
36
58
|
*/
|
|
37
59
|
export class AccountAdmissionRefusedError extends Error {
|
|
38
60
|
readonly status = 403;
|
|
61
|
+
/** The person was put on file, switched off, for an admin to switch on (see `waitForAdmin`). */
|
|
62
|
+
readonly waitingForAdmin: boolean;
|
|
39
63
|
|
|
40
|
-
constructor(message: string) {
|
|
64
|
+
constructor(message: string, opts: { waitingForAdmin?: boolean } = {}) {
|
|
41
65
|
super(message);
|
|
42
66
|
this.name = 'AccountAdmissionRefusedError';
|
|
67
|
+
this.waitingForAdmin = opts.waitingForAdmin ?? false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The account exists and an admin switched it off — at sign-in, or for a
|
|
73
|
+
* credential it still holds. A 403 like a refusal: the identity is fine,
|
|
74
|
+
* the deployment no longer lets it in.
|
|
75
|
+
*/
|
|
76
|
+
export class AccountDeactivatedError extends Error {
|
|
77
|
+
readonly status = 403;
|
|
78
|
+
|
|
79
|
+
constructor(message = ACCOUNT_DEACTIVATED_MESSAGE) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'AccountDeactivatedError';
|
|
43
82
|
}
|
|
44
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether the account behind a credential is on could not be asked — the
|
|
87
|
+
* database failed. Not the caller's fault and not a verdict on them: a
|
|
88
|
+
* route answers it with a 500, never with the 401 that would sign a valid
|
|
89
|
+
* caller out during an outage.
|
|
90
|
+
*/
|
|
91
|
+
export class AuthBackendError extends Error {
|
|
92
|
+
constructor(readonly cause: unknown) {
|
|
93
|
+
super('The account could not be looked up');
|
|
94
|
+
this.name = 'AuthBackendError';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An admin's change to an account that the deployment refuses on purpose
|
|
100
|
+
* (switching off the deployment admin, or an account the platform runs its
|
|
101
|
+
* own work as). Its message is for the admin; a route answers it with 400.
|
|
102
|
+
*/
|
|
103
|
+
export class AccountChangeRefusedError extends Error {
|
|
104
|
+
readonly status = 400;
|
|
105
|
+
|
|
106
|
+
constructor(message: string) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = 'AccountChangeRefusedError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** What a person with a deactivated account is told, wherever they knock. */
|
|
113
|
+
export const ACCOUNT_DEACTIVATED_MESSAGE = 'Your account on this workspace is switched off. Ask its admin to turn it back on.';
|
|
@@ -6,7 +6,7 @@ import { printable } from '../../shared/printable.js';
|
|
|
6
6
|
const log = logger('accounts');
|
|
7
7
|
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
8
8
|
import type { AuthService } from './auth.service.js';
|
|
9
|
-
import { AccountAdmissionRefusedError } from './account-admission.js';
|
|
9
|
+
import { AccountAdmissionRefusedError, AccountChangeRefusedError } from './account-admission.js';
|
|
10
10
|
import { erasedAccountId, type IAccountErasureService } from './account-erasure.service.js';
|
|
11
11
|
import type { IAdminAccessService } from '../admin/admin.interface.js';
|
|
12
12
|
import type { UserAccessRemovalService } from '../access/user-access-removal.service.js';
|
|
@@ -26,7 +26,7 @@ import './auth.middleware.js'; // Express Request augmentation
|
|
|
26
26
|
* bootstrap admin (always-admin, see AdminAccessService).
|
|
27
27
|
*/
|
|
28
28
|
export function createAccountRoutes(
|
|
29
|
-
authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById'>,
|
|
29
|
+
authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById' | 'deactivate' | 'reactivate'>,
|
|
30
30
|
adminAccess: IAdminAccessService,
|
|
31
31
|
accountErasure: Pick<IAccountErasureService, 'eraseUser'>,
|
|
32
32
|
accessRemoval?: Pick<UserAccessRemovalService, 'report' | 'assertRemovable' | 'remove' | 'filesNaming'>,
|
|
@@ -47,17 +47,23 @@ export function createAccountRoutes(
|
|
|
47
47
|
res.json({ accounts: await authService.listAccounts() });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// POST /api/admin/accounts { email, name?, password } — create an account
|
|
50
|
+
// POST /api/admin/accounts { email, name?, password? } — create an account
|
|
51
51
|
// (or reset the password of an existing one; upsert-by-email is deliberate,
|
|
52
|
-
// see AuthService.createAccount).
|
|
52
|
+
// see AuthService.createAccount). Without a password the account is for
|
|
53
|
+
// single sign-on: the person finds it waiting when they first sign in.
|
|
53
54
|
router.post('/admin/accounts', requireAdmin, async (req, res) => {
|
|
54
55
|
const { email, name, password } = req.body as {
|
|
55
56
|
email?: string;
|
|
56
57
|
name?: string;
|
|
57
|
-
password?:
|
|
58
|
+
password?: unknown;
|
|
58
59
|
};
|
|
59
|
-
if (!email
|
|
60
|
-
res.status(400).json({ error: 'email
|
|
60
|
+
if (!email) {
|
|
61
|
+
res.status(400).json({ error: 'email is required' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Absent means single sign-on; anything else that is not text is a mistake, not that.
|
|
65
|
+
if (password !== undefined && typeof password !== 'string') {
|
|
66
|
+
res.status(400).json({ error: 'password must be a string' });
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
63
69
|
try {
|
|
@@ -71,6 +77,56 @@ export function createAccountRoutes(
|
|
|
71
77
|
}
|
|
72
78
|
});
|
|
73
79
|
|
|
80
|
+
// POST /api/admin/accounts/:userId/deactivate — switch an account off. It
|
|
81
|
+
// keeps its row, history and place in roles and groups; nothing it holds
|
|
82
|
+
// signs in any more (see AuthService.isActive). An admin's own account is
|
|
83
|
+
// refused, so whoever does this is still an admin who is on afterwards —
|
|
84
|
+
// the deployment is never left with no admin able to sign in.
|
|
85
|
+
router.post('/admin/accounts/:userId/deactivate', requireAdmin, async (req, res) => {
|
|
86
|
+
const userId = String(req.params.userId);
|
|
87
|
+
if (userId === req.userId) {
|
|
88
|
+
res.status(400).json({ error: 'You cannot switch off your own account. Ask another admin.' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (!(await authService.deactivate(userId))) {
|
|
93
|
+
res.status(404).json({ error: 'No such user' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
log.info('account audit:', { action: 'deactivate', actorUserId: req.userId, targetUserId: userId });
|
|
97
|
+
res.status(204).end();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof AccountChangeRefusedError) {
|
|
100
|
+
res.status(400).json({ error: error.message });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
log.error('switching an account off failed:', { err: error });
|
|
104
|
+
res.status(500).json({ error: 'Could not switch this account off' });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// POST /api/admin/accounts/:userId/reactivate — switch it back on, if the
|
|
109
|
+
// deployment has room: the admission port is asked, as for a new account
|
|
110
|
+
// (a host that sells seats answers from its plan; 403 with its words).
|
|
111
|
+
router.post('/admin/accounts/:userId/reactivate', requireAdmin, async (req, res) => {
|
|
112
|
+
const userId = String(req.params.userId);
|
|
113
|
+
try {
|
|
114
|
+
if (!(await authService.reactivate(userId))) {
|
|
115
|
+
res.status(404).json({ error: 'No such user' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
log.info('account audit:', { action: 'reactivate', actorUserId: req.userId, targetUserId: userId });
|
|
119
|
+
res.status(204).end();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof AccountAdmissionRefusedError) {
|
|
122
|
+
res.status(403).json({ error: error.message });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
log.error('switching an account on failed:', { err: error });
|
|
126
|
+
res.status(500).json({ error: 'Could not switch this account on' });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
74
130
|
// GET /api/admin/accounts/:userId/references — how many places in the
|
|
75
131
|
// knowledge base name this account's address (roles, groups, access rules,
|
|
76
132
|
// file grants) and whether removing them is allowed. Feeds the delete
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import type { AuthService } from './auth.service.js';
|
|
3
|
+
import { AccountDeactivatedError, AuthBackendError } from './account-admission.js';
|
|
3
4
|
|
|
4
5
|
// Augment Express Request so userId/userEmail are available after auth middleware
|
|
5
6
|
declare global {
|
|
@@ -46,8 +47,8 @@ function readAuthCookie(req: Request): string | null {
|
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function createAuthMiddleware(authService: AuthService) {
|
|
50
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
50
|
+
export function createAuthMiddleware(authService: Pick<AuthService, 'resolveSession'>) {
|
|
51
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
51
52
|
// Bearer is the primary path — every JSON API call in the frontend
|
|
52
53
|
// attaches it via `authFetch`. Cookie is the fallback for transports
|
|
53
54
|
// that can't carry headers (EventSource, image tags), set at login by
|
|
@@ -66,13 +67,23 @@ export function createAuthMiddleware(authService: AuthService) {
|
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
let session: { userId: string; email: string };
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
session = await authService.resolveSession(token);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// The account could not be looked up: not the caller's doing, and a
|
|
75
|
+
// 401 here would sign every valid session out during an outage.
|
|
76
|
+
if (err instanceof AuthBackendError) {
|
|
77
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// A 401 otherwise, so the app drops the session and shows the login
|
|
81
|
+
// page; a switched-off account is told why there.
|
|
82
|
+
res.status(401).json({ error: err instanceof AccountDeactivatedError ? err.message : 'Invalid or expired token' });
|
|
83
|
+
return;
|
|
76
84
|
}
|
|
85
|
+
req.userId = session.userId;
|
|
86
|
+
req.userEmail = session.email;
|
|
87
|
+
next();
|
|
77
88
|
};
|
|
78
89
|
}
|
|
@@ -5,6 +5,7 @@ const log = logger('auth');
|
|
|
5
5
|
import type { AuthService } from './auth.service.js';
|
|
6
6
|
import { AUTH_COOKIE_NAME } from './auth.middleware.js'; // also imports Express Request augmentation
|
|
7
7
|
import { FixedWindowRateLimiter } from './rate-limit.js';
|
|
8
|
+
import { AccountDeactivatedError } from './account-admission.js';
|
|
8
9
|
import { canonicalEmail } from '../../shared/email-identity.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -113,6 +114,13 @@ export function createAuthRoutes(
|
|
|
113
114
|
});
|
|
114
115
|
res.json(result);
|
|
115
116
|
} catch (error) {
|
|
117
|
+
// The password was right and an admin switched the account off: said
|
|
118
|
+
// as such (only ever after the password is proven, see
|
|
119
|
+
// `loginWithPassword`), so the person knows whom to ask.
|
|
120
|
+
if (error instanceof AccountDeactivatedError) {
|
|
121
|
+
res.status(403).json({ error: error.message });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
117
125
|
log.error('Login error:', { detail: msg });
|
|
118
126
|
res.status(401).json({ error: 'Authentication failed' });
|