@aphexcms/cms-core 9.8.0 → 9.9.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/api/organizations.d.ts +2 -1
- package/dist/api/organizations.d.ts.map +1 -1
- package/dist/api/organizations.js +2 -1
- package/dist/api/schemas/jobs.d.ts +15 -0
- package/dist/api/schemas/jobs.d.ts.map +1 -0
- package/dist/api/schemas/jobs.js +17 -0
- package/dist/api/schemas/user.d.ts.map +1 -1
- package/dist/api/schemas/user.js +6 -2
- package/dist/auth/account-deletion.d.ts +35 -0
- package/dist/auth/account-deletion.d.ts.map +1 -0
- package/dist/auth/account-deletion.js +65 -0
- package/dist/auth/bootstrap.d.ts +104 -0
- package/dist/auth/bootstrap.d.ts.map +1 -0
- package/dist/auth/bootstrap.js +164 -0
- package/dist/auth/instance-state.d.ts +13 -0
- package/dist/auth/instance-state.d.ts.map +1 -0
- package/dist/auth/instance-state.js +30 -0
- package/dist/auth/invitation-status.d.ts +25 -0
- package/dist/auth/invitation-status.d.ts.map +1 -0
- package/dist/auth/invitation-status.js +41 -0
- package/dist/auth/sign-up-policy.d.ts +62 -0
- package/dist/auth/sign-up-policy.d.ts.map +1 -0
- package/dist/auth/sign-up-policy.js +120 -0
- package/dist/components/admin/ActivityView.svelte +263 -13
- package/dist/components/admin/ActivityView.svelte.d.ts +10 -1
- package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
- package/dist/db/interfaces/events.d.ts +29 -2
- package/dist/db/interfaces/events.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +1 -0
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/index.js +1 -0
- package/dist/db/interfaces/instance.d.ts +29 -0
- package/dist/db/interfaces/instance.d.ts.map +1 -1
- package/dist/db/interfaces/instance.js +8 -1
- package/dist/events/built-in-consumers.d.ts +20 -0
- package/dist/events/built-in-consumers.d.ts.map +1 -0
- package/dist/events/built-in-consumers.js +49 -0
- package/dist/events/catalog.d.ts +16 -0
- package/dist/events/catalog.d.ts.map +1 -1
- package/dist/events/catalog.js +17 -0
- package/dist/events/consumer.d.ts +14 -0
- package/dist/events/consumer.d.ts.map +1 -1
- package/dist/events/consumer.js +1 -0
- package/dist/events/emit.d.ts +23 -0
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +32 -1
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +2 -0
- package/dist/field-validation/utils.d.ts +27 -4
- package/dist/field-validation/utils.d.ts.map +1 -1
- package/dist/field-validation/utils.js +78 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/jobs/run-batch.d.ts +7 -1
- package/dist/jobs/run-batch.d.ts.map +1 -1
- package/dist/jobs/run-batch.js +3 -1
- package/dist/lib/api/organizations.d.ts +2 -1
- package/dist/lib/api/organizations.d.ts.map +1 -1
- package/dist/lib/api/organizations.js +2 -1
- package/dist/lib/api/organizations.js.map +1 -1
- package/dist/lib/api/schemas/api-keys.d.ts +4 -4
- package/dist/lib/api/schemas/jobs.d.ts +15 -0
- package/dist/lib/api/schemas/jobs.d.ts.map +1 -0
- package/dist/lib/api/schemas/jobs.js +18 -0
- package/dist/lib/api/schemas/jobs.js.map +1 -0
- package/dist/lib/api/schemas/user.d.ts.map +1 -1
- package/dist/lib/api/schemas/user.js +6 -2
- package/dist/lib/api/schemas/user.js.map +1 -1
- package/dist/lib/auth/account-deletion.d.ts +35 -0
- package/dist/lib/auth/account-deletion.d.ts.map +1 -0
- package/dist/lib/auth/account-deletion.js +66 -0
- package/dist/lib/auth/account-deletion.js.map +1 -0
- package/dist/lib/auth/bootstrap.d.ts +104 -0
- package/dist/lib/auth/bootstrap.d.ts.map +1 -0
- package/dist/lib/auth/bootstrap.js +165 -0
- package/dist/lib/auth/bootstrap.js.map +1 -0
- package/dist/lib/auth/instance-state.d.ts +13 -0
- package/dist/lib/auth/instance-state.d.ts.map +1 -0
- package/dist/lib/auth/instance-state.js +31 -0
- package/dist/lib/auth/instance-state.js.map +1 -0
- package/dist/lib/auth/invitation-status.d.ts +25 -0
- package/dist/lib/auth/invitation-status.d.ts.map +1 -0
- package/dist/lib/auth/invitation-status.js +42 -0
- package/dist/lib/auth/invitation-status.js.map +1 -0
- package/dist/lib/auth/sign-up-policy.d.ts +62 -0
- package/dist/lib/auth/sign-up-policy.d.ts.map +1 -0
- package/dist/lib/auth/sign-up-policy.js +121 -0
- package/dist/lib/auth/sign-up-policy.js.map +1 -0
- package/dist/lib/db/interfaces/events.d.ts +29 -2
- package/dist/lib/db/interfaces/events.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +1 -0
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.js +1 -0
- package/dist/lib/db/interfaces/index.js.map +1 -1
- package/dist/lib/db/interfaces/instance.d.ts +29 -0
- package/dist/lib/db/interfaces/instance.d.ts.map +1 -1
- package/dist/lib/db/interfaces/instance.js +8 -1
- package/dist/lib/db/interfaces/instance.js.map +1 -1
- package/dist/lib/events/built-in-consumers.d.ts +20 -0
- package/dist/lib/events/built-in-consumers.d.ts.map +1 -0
- package/dist/lib/events/built-in-consumers.js +50 -0
- package/dist/lib/events/built-in-consumers.js.map +1 -0
- package/dist/lib/events/catalog.d.ts +16 -0
- package/dist/lib/events/catalog.d.ts.map +1 -1
- package/dist/lib/events/catalog.js +17 -0
- package/dist/lib/events/catalog.js.map +1 -1
- package/dist/lib/events/consumer.d.ts +14 -0
- package/dist/lib/events/consumer.d.ts.map +1 -1
- package/dist/lib/events/consumer.js +1 -0
- package/dist/lib/events/consumer.js.map +1 -1
- package/dist/lib/events/emit.d.ts +23 -0
- package/dist/lib/events/emit.d.ts.map +1 -1
- package/dist/lib/events/emit.js +32 -1
- package/dist/lib/events/emit.js.map +1 -1
- package/dist/lib/events/index.d.ts +2 -0
- package/dist/lib/events/index.d.ts.map +1 -1
- package/dist/lib/events/index.js +2 -0
- package/dist/lib/events/index.js.map +1 -1
- package/dist/lib/field-validation/utils.d.ts +27 -4
- package/dist/lib/field-validation/utils.d.ts.map +1 -1
- package/dist/lib/field-validation/utils.js +78 -10
- package/dist/lib/field-validation/utils.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/jobs/run-batch.d.ts +7 -1
- package/dist/lib/jobs/run-batch.d.ts.map +1 -1
- package/dist/lib/jobs/run-batch.js +3 -1
- package/dist/lib/jobs/run-batch.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +42 -1
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +68 -8
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts +1 -1
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js +1 -1
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/plugins/resolver.d.ts.map +1 -1
- package/dist/lib/plugins/resolver.js +7 -1
- package/dist/lib/plugins/resolver.js.map +1 -1
- package/dist/lib/server/api/rate-limit.d.ts +38 -0
- package/dist/lib/server/api/rate-limit.d.ts.map +1 -0
- package/dist/lib/server/api/rate-limit.js +71 -0
- package/dist/lib/server/api/rate-limit.js.map +1 -0
- package/dist/lib/server/api/routes/agent-change-sets.js +1 -1
- package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -1
- package/dist/lib/server/api/routes/document-versions.js +1 -1
- package/dist/lib/server/api/routes/document-versions.js.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.js +11 -2
- package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.js +11 -1
- package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
- package/dist/lib/server/api/routes/documents.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents.js +10 -0
- package/dist/lib/server/api/routes/documents.js.map +1 -1
- package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/lib/server/api/routes/jobs.js +174 -20
- package/dist/lib/server/api/routes/jobs.js.map +1 -1
- package/dist/lib/server/api/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/organizations-by-id.js +61 -3
- package/dist/lib/server/api/routes/organizations-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/lib/server/api/routes/organizations-invitations.js +31 -2
- package/dist/lib/server/api/routes/organizations-invitations.js.map +1 -1
- package/dist/lib/server/api/routes/resolve-organization-names.d.ts +17 -0
- package/dist/lib/server/api/routes/resolve-organization-names.d.ts.map +1 -0
- package/dist/lib/server/api/routes/resolve-organization-names.js +26 -0
- package/dist/lib/server/api/routes/resolve-organization-names.js.map +1 -0
- package/dist/lib/server/api/routes/user.d.ts +0 -8
- package/dist/lib/server/api/routes/user.d.ts.map +1 -1
- package/dist/lib/server/api/routes/user.js +144 -0
- package/dist/lib/server/api/routes/user.js.map +1 -1
- package/dist/lib/server/index.d.ts +6 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +9 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/references-service.d.ts +1 -1
- package/dist/lib/services/references-service.d.ts.map +1 -1
- package/dist/lib/types/capabilities.d.ts +14 -0
- package/dist/lib/types/capabilities.d.ts.map +1 -1
- package/dist/lib/types/capabilities.js +37 -18
- package/dist/lib/types/capabilities.js.map +1 -1
- package/dist/lib/types/events.d.ts +27 -4
- package/dist/lib/types/events.d.ts.map +1 -1
- package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -1
- package/dist/lib/utils/fetch-remote-file.js +32 -3
- package/dist/lib/utils/fetch-remote-file.js.map +1 -1
- package/dist/local-api/collection-api.d.ts +42 -1
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +68 -8
- package/dist/local-api/index.d.ts +1 -1
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/index.js +1 -1
- package/dist/plugins/resolver.d.ts.map +1 -1
- package/dist/plugins/resolver.js +7 -1
- package/dist/server/api/rate-limit.d.ts +38 -0
- package/dist/server/api/rate-limit.d.ts.map +1 -0
- package/dist/server/api/rate-limit.js +70 -0
- package/dist/server/api/routes/agent-change-sets.js +1 -1
- package/dist/server/api/routes/document-versions.js +1 -1
- package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/documents-by-id.js +11 -2
- package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/server/api/routes/documents-publish.js +11 -1
- package/dist/server/api/routes/documents.d.ts.map +1 -1
- package/dist/server/api/routes/documents.js +10 -0
- package/dist/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/server/api/routes/jobs.js +174 -20
- package/dist/server/api/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/organizations-by-id.js +61 -3
- package/dist/server/api/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/server/api/routes/organizations-invitations.js +31 -2
- package/dist/server/api/routes/resolve-organization-names.d.ts +17 -0
- package/dist/server/api/routes/resolve-organization-names.d.ts.map +1 -0
- package/dist/server/api/routes/resolve-organization-names.js +25 -0
- package/dist/server/api/routes/user.d.ts +0 -8
- package/dist/server/api/routes/user.d.ts.map +1 -1
- package/dist/server/api/routes/user.js +144 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -1
- package/dist/services/references-service.d.ts +1 -1
- package/dist/services/references-service.d.ts.map +1 -1
- package/dist/types/capabilities.d.ts +14 -0
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/capabilities.js +37 -18
- package/dist/types/events.d.ts +27 -4
- package/dist/types/events.d.ts.map +1 -1
- package/dist/utils/fetch-remote-file.d.ts.map +1 -1
- package/dist/utils/fetch-remote-file.js +32 -3
- package/package.json +6 -6
|
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
4
4
|
import { hasCapability } from '../../../types/capabilities.js';
|
|
5
|
-
import { RevisionConflictError } from '../../../db/interfaces.js';
|
|
5
|
+
import { RevisionConflictError } from '../../../db/interfaces/index.js';
|
|
6
6
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
7
7
|
import { withCreatedByNames } from './resolve-created-by.js';
|
|
8
8
|
const listChangeSetsQuery = z.object({
|
|
@@ -3,7 +3,7 @@ import { zValidator } from '@hono/zod-validator';
|
|
|
3
3
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
4
4
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
5
5
|
import { hasCapability } from '../../../types/capabilities.js';
|
|
6
|
-
import { RevisionConflictError } from '../../../db/interfaces.js';
|
|
6
|
+
import { RevisionConflictError } from '../../../db/interfaces/index.js';
|
|
7
7
|
import { listVersionsQuery, restoreVersionRequest } from '../../../api/schemas/documents.js';
|
|
8
8
|
export const documentVersionsRouter = new Hono()
|
|
9
9
|
.get('/:id/versions', zValidator('query', listVersionsQuery, (result, c) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents-by-id.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents-by-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"documents-by-id.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents-by-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,QAAQ,CA6O5C,CAAC"}
|
|
@@ -2,8 +2,8 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { zValidator } from '@hono/zod-validator';
|
|
3
3
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
4
4
|
import { PermissionError } from '../../../local-api/permissions.js';
|
|
5
|
-
import { SingletonOperationError } from '../../../local-api/collection-api.js';
|
|
6
|
-
import { RevisionConflictError } from '../../../db/interfaces.js';
|
|
5
|
+
import { SingletonOperationError, DocumentValidationError } from '../../../local-api/collection-api.js';
|
|
6
|
+
import { RevisionConflictError } from '../../../db/interfaces/index.js';
|
|
7
7
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
8
8
|
import { updateDocumentRequest } from '../../../api/schemas/documents.js';
|
|
9
9
|
export const documentsByIdRouter = new Hono()
|
|
@@ -108,6 +108,15 @@ export const documentsByIdRouter = new Hono()
|
|
|
108
108
|
currentRevision: error.currentRevision
|
|
109
109
|
}, 409);
|
|
110
110
|
}
|
|
111
|
+
// A malformed payload is the caller's fault, not the server's.
|
|
112
|
+
if (error instanceof DocumentValidationError) {
|
|
113
|
+
return c.json({
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'Validation failed',
|
|
116
|
+
message: error.message,
|
|
117
|
+
issues: error.errors
|
|
118
|
+
}, 400);
|
|
119
|
+
}
|
|
111
120
|
if (error instanceof Error && error.message.includes('validation errors')) {
|
|
112
121
|
return c.json({ success: false, error: 'Validation failed', message: error.message }, 400);
|
|
113
122
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents-publish.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents-publish.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"documents-publish.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents-publish.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,eAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC,QAAQ,CA8U/C,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
3
3
|
import { PermissionError } from '../../../local-api/permissions.js';
|
|
4
|
-
import { RevisionConflictError } from '../../../db/interfaces.js';
|
|
4
|
+
import { RevisionConflictError } from '../../../db/interfaces/index.js';
|
|
5
|
+
import { DocumentValidationError } from '../../../local-api/collection-api.js';
|
|
5
6
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
6
7
|
import { scheduleDocumentRequest, publishDocumentRequest, unpublishDocumentRequest } from '../../../api/schemas/documents.js';
|
|
7
8
|
export const documentsPublishRouter = new Hono()
|
|
@@ -178,6 +179,15 @@ export const documentsPublishRouter = new Hono()
|
|
|
178
179
|
currentRevision: error.currentRevision
|
|
179
180
|
}, 409);
|
|
180
181
|
}
|
|
182
|
+
// A malformed payload is the caller's fault, not the server's.
|
|
183
|
+
if (error instanceof DocumentValidationError) {
|
|
184
|
+
return c.json({
|
|
185
|
+
success: false,
|
|
186
|
+
error: 'Validation failed',
|
|
187
|
+
message: error.message,
|
|
188
|
+
issues: error.errors
|
|
189
|
+
}, 400);
|
|
190
|
+
}
|
|
181
191
|
if (error instanceof Error && error.message.includes('validation errors')) {
|
|
182
192
|
return c.json({
|
|
183
193
|
success: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAKzC,eAAO,MAAM,eAAe,EAAE,IAAI,CAAC,QAAQ,CAyNzC,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { zValidator } from '@hono/zod-validator';
|
|
3
3
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
4
4
|
import { PermissionError } from '../../../local-api/permissions.js';
|
|
5
|
+
import { DocumentValidationError } from '../../../local-api/collection-api.js';
|
|
5
6
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
6
7
|
import { createDocumentRequest, getDocumentsByIdsQuery, listDocumentsQuery } from '../../../api/schemas/documents.js';
|
|
7
8
|
const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -148,6 +149,15 @@ export const documentsRouter = new Hono()
|
|
|
148
149
|
if (error instanceof PermissionError) {
|
|
149
150
|
return c.json({ success: false, error: 'Forbidden', message: error.message }, 403);
|
|
150
151
|
}
|
|
152
|
+
// A malformed payload is the caller's fault, not the server's.
|
|
153
|
+
if (error instanceof DocumentValidationError) {
|
|
154
|
+
return c.json({
|
|
155
|
+
success: false,
|
|
156
|
+
error: 'Validation failed',
|
|
157
|
+
message: error.message,
|
|
158
|
+
issues: error.errors
|
|
159
|
+
}, 400);
|
|
160
|
+
}
|
|
151
161
|
if (error instanceof Error && error.message.includes('validation errors')) {
|
|
152
162
|
return c.json({ success: false, error: 'Validation failed', message: error.message }, 400);
|
|
153
163
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AA6HzC,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,QAAQ,CAuKnC,CAAC"}
|
|
@@ -2,23 +2,35 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { authToContext } from '../../../local-api/auth-helpers.js';
|
|
4
4
|
import { hasCapability } from '../../../types/capabilities.js';
|
|
5
|
+
import { jobActionRequestSchema } from '../../../api/schemas/jobs.js';
|
|
5
6
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
6
7
|
import { withCreatedByNames } from './resolve-created-by.js';
|
|
7
|
-
|
|
8
|
-
//
|
|
8
|
+
import { withOrganizationNames } from './resolve-organization-names.js';
|
|
9
|
+
// Read-only history / observability for the durable spine, plus the two operator actions
|
|
10
|
+
// that make a dead letter recoverable. Reads are gated on `document.read`: the operational
|
|
11
|
+
// log of publish work is visible to anyone who can read content. Mutations are gated on
|
|
12
|
+
// `org.settings` — see `requireJobControl`.
|
|
9
13
|
// Mirrors JobStatus (types/events) as a zod enum — the API contract's source of truth.
|
|
10
14
|
const jobStatus = z.enum(['pending', 'leased', 'completed', 'failed', 'cancelled']);
|
|
15
|
+
/**
|
|
16
|
+
* `?scope=all` widens a history read from the active organization to the whole instance.
|
|
17
|
+
* Super admins only — it's the one place the admin UI crosses a tenant boundary.
|
|
18
|
+
*/
|
|
19
|
+
const historyScope = z.enum(['organization', 'all']);
|
|
11
20
|
const listJobsQuery = z.object({
|
|
12
21
|
status: jobStatus.optional(),
|
|
13
22
|
type: z.string().optional(),
|
|
23
|
+
scope: historyScope.optional(),
|
|
14
24
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
15
25
|
offset: z.coerce.number().int().min(0).optional()
|
|
16
26
|
});
|
|
17
27
|
const listEventsQuery = z.object({
|
|
18
28
|
type: z.string().optional(),
|
|
29
|
+
scope: historyScope.optional(),
|
|
19
30
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
20
31
|
offset: z.coerce.number().int().min(0).optional()
|
|
21
32
|
});
|
|
33
|
+
const healthQuery = z.object({ scope: historyScope.optional() });
|
|
22
34
|
/** Map an adapter Page into the ApiResponse `pagination` shape the client expects. */
|
|
23
35
|
function toPagination(page) {
|
|
24
36
|
const pageSize = page.limit || 1;
|
|
@@ -31,6 +43,9 @@ function toPagination(page) {
|
|
|
31
43
|
hasPrevPage: page.offset > 0
|
|
32
44
|
};
|
|
33
45
|
}
|
|
46
|
+
function isSuperAdmin(auth) {
|
|
47
|
+
return auth.type === 'session' && auth.user.role === 'super_admin';
|
|
48
|
+
}
|
|
34
49
|
function requireHistoryAccess(c) {
|
|
35
50
|
const auth = c.var.auth;
|
|
36
51
|
if (!auth || auth.type === 'partial_session') {
|
|
@@ -39,7 +54,55 @@ function requireHistoryAccess(c) {
|
|
|
39
54
|
if (!hasCapability(auth, 'document.read')) {
|
|
40
55
|
return { error: c.json({ success: false, error: 'Insufficient permissions' }, 403) };
|
|
41
56
|
}
|
|
42
|
-
return { organizationId: authToContext(auth).organizationId };
|
|
57
|
+
return { auth, organizationId: authToContext(auth).organizationId };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Gate for retry/cancel — `org.settings`, which only `admin` and `owner` hold.
|
|
61
|
+
*
|
|
62
|
+
* Requeueing a job *runs* it, and a job can be anything: a scheduled publish, a GDPR
|
|
63
|
+
* erasure, a plugin's webhook delivery. There's no single content capability that covers
|
|
64
|
+
* that surface, so the gate is the operational one. It's not a widening: both roles that
|
|
65
|
+
* hold `org.settings` already hold every document and asset capability, so retrying can't
|
|
66
|
+
* let anyone cause an effect they couldn't cause directly.
|
|
67
|
+
*/
|
|
68
|
+
function requireJobControl(c) {
|
|
69
|
+
const auth = c.var.auth;
|
|
70
|
+
if (!auth || auth.type === 'partial_session') {
|
|
71
|
+
return { error: c.json({ success: false, error: 'Authentication required' }, 401) };
|
|
72
|
+
}
|
|
73
|
+
if (!hasCapability(auth, 'org.settings')) {
|
|
74
|
+
return { error: c.json({ success: false, error: 'Insufficient permissions' }, 403) };
|
|
75
|
+
}
|
|
76
|
+
return { auth, organizationId: authToContext(auth).organizationId };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Which organization a read covers: the caller's, or every one of them.
|
|
80
|
+
*
|
|
81
|
+
* `undefined` means instance-wide to the adapter, which bypasses RLS — so this returns it
|
|
82
|
+
* only for a super admin who explicitly asked. Anyone else asking for `scope=all` is quietly
|
|
83
|
+
* scoped back to their own org rather than refused: the parameter is a view preference, and
|
|
84
|
+
* failing the whole page over it would be a worse experience than showing what they can see.
|
|
85
|
+
*/
|
|
86
|
+
function readScope(auth, organizationId, scope) {
|
|
87
|
+
return scope === 'all' && isSuperAdmin(auth) ? undefined : organizationId;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve which organization an action targets, and prove the caller may act there.
|
|
91
|
+
*
|
|
92
|
+
* A body `organizationId` is only honored for a super admin acting from the instance-wide
|
|
93
|
+
* view. For everyone else it must match the active organization — a mismatch is a 403, not a
|
|
94
|
+
* silent fallback, because a body that names another tenant is either a bug or an attempt.
|
|
95
|
+
*/
|
|
96
|
+
function resolveActionOrganization(c, auth, activeOrganizationId, requested) {
|
|
97
|
+
if (!requested || requested === activeOrganizationId) {
|
|
98
|
+
return { organizationId: activeOrganizationId };
|
|
99
|
+
}
|
|
100
|
+
if (!isSuperAdmin(auth)) {
|
|
101
|
+
return {
|
|
102
|
+
error: c.json({ success: false, error: 'Cannot act on jobs outside your active organization' }, 403)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { organizationId: requested };
|
|
43
106
|
}
|
|
44
107
|
export const jobsRouter = new Hono()
|
|
45
108
|
.get('/jobs', async (c) => {
|
|
@@ -51,21 +114,115 @@ export const jobsRouter = new Hono()
|
|
|
51
114
|
if (!parsed.success) {
|
|
52
115
|
return c.json({ success: false, error: 'Invalid query', issues: parsed.error.issues }, 400);
|
|
53
116
|
}
|
|
54
|
-
const { status, type, limit, offset } = parsed.data;
|
|
117
|
+
const { status, type, scope, limit, offset } = parsed.data;
|
|
118
|
+
const organizationId = readScope(gate.auth, gate.organizationId, scope);
|
|
55
119
|
const { databaseAdapter } = c.var.aphexCMS;
|
|
56
|
-
const page = await databaseAdapter.listJobs({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
return c.json({ success: true, data: page.items, pagination: toPagination(page) });
|
|
120
|
+
const page = await databaseAdapter.listJobs({ organizationId, status, type, limit, offset });
|
|
121
|
+
// Only the instance-wide view needs org names; org-scoped rows are all the same org.
|
|
122
|
+
const items = organizationId
|
|
123
|
+
? page.items
|
|
124
|
+
: await withOrganizationNames(page.items, databaseAdapter);
|
|
125
|
+
return c.json({ success: true, data: items, pagination: toPagination(page) });
|
|
64
126
|
}
|
|
65
127
|
catch (error) {
|
|
66
128
|
cmsLogger.error('Failed to list jobs:', error);
|
|
67
129
|
return c.json({ success: false, error: 'Failed to list jobs' }, 500);
|
|
68
130
|
}
|
|
131
|
+
})
|
|
132
|
+
.get('/jobs/health', async (c) => {
|
|
133
|
+
try {
|
|
134
|
+
const gate = requireHistoryAccess(c);
|
|
135
|
+
if ('error' in gate)
|
|
136
|
+
return gate.error;
|
|
137
|
+
const parsed = healthQuery.safeParse(c.req.query());
|
|
138
|
+
if (!parsed.success) {
|
|
139
|
+
return c.json({ success: false, error: 'Invalid query', issues: parsed.error.issues }, 400);
|
|
140
|
+
}
|
|
141
|
+
const organizationId = readScope(gate.auth, gate.organizationId, parsed.data.scope);
|
|
142
|
+
const { databaseAdapter } = c.var.aphexCMS;
|
|
143
|
+
const health = await databaseAdapter.outboxHealth({ organizationId });
|
|
144
|
+
return c.json({ success: true, data: health });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
cmsLogger.error('Failed to read outbox health:', error);
|
|
148
|
+
return c.json({ success: false, error: 'Failed to read outbox health' }, 500);
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.post('/jobs/:id/retry', async (c) => {
|
|
152
|
+
try {
|
|
153
|
+
const gate = requireJobControl(c);
|
|
154
|
+
if ('error' in gate)
|
|
155
|
+
return gate.error;
|
|
156
|
+
const body = await c.req.json().catch(() => ({}));
|
|
157
|
+
const parsed = jobActionRequestSchema.safeParse(body);
|
|
158
|
+
if (!parsed.success) {
|
|
159
|
+
return c.json({ success: false, error: 'Invalid request', issues: parsed.error.issues }, 400);
|
|
160
|
+
}
|
|
161
|
+
const target = resolveActionOrganization(c, gate.auth, gate.organizationId, parsed.data.organizationId);
|
|
162
|
+
if ('error' in target)
|
|
163
|
+
return target.error;
|
|
164
|
+
const id = c.req.param('id');
|
|
165
|
+
const { databaseAdapter } = c.var.aphexCMS;
|
|
166
|
+
const job = await databaseAdapter.requeueJob(target.organizationId, id, {
|
|
167
|
+
runAt: new Date()
|
|
168
|
+
});
|
|
169
|
+
if (!job) {
|
|
170
|
+
// The guard matched nothing: either the job isn't there, or it's in a state that
|
|
171
|
+
// can't be requeued. Read it back so the message says which — "already running"
|
|
172
|
+
// and "no such job" call for completely different reactions from an operator.
|
|
173
|
+
const existing = await databaseAdapter.getJob(target.organizationId, id);
|
|
174
|
+
if (!existing)
|
|
175
|
+
return c.json({ success: false, error: 'Job not found' }, 404);
|
|
176
|
+
return c.json({
|
|
177
|
+
success: false,
|
|
178
|
+
error: `Only failed or cancelled jobs can be retried — this one is ${existing.status}.`
|
|
179
|
+
}, 409);
|
|
180
|
+
}
|
|
181
|
+
cmsLogger.info(`[jobs] Job ${id} (${job.type}) requeued by ${gate.auth.type === 'session' ? gate.auth.user.id : 'api key'}`);
|
|
182
|
+
return c.json({ success: true, data: job });
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
cmsLogger.error('Failed to retry job:', error);
|
|
186
|
+
return c.json({ success: false, error: 'Failed to retry job' }, 500);
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.post('/jobs/:id/cancel', async (c) => {
|
|
190
|
+
try {
|
|
191
|
+
const gate = requireJobControl(c);
|
|
192
|
+
if ('error' in gate)
|
|
193
|
+
return gate.error;
|
|
194
|
+
const body = await c.req.json().catch(() => ({}));
|
|
195
|
+
const parsed = jobActionRequestSchema.safeParse(body);
|
|
196
|
+
if (!parsed.success) {
|
|
197
|
+
return c.json({ success: false, error: 'Invalid request', issues: parsed.error.issues }, 400);
|
|
198
|
+
}
|
|
199
|
+
const target = resolveActionOrganization(c, gate.auth, gate.organizationId, parsed.data.organizationId);
|
|
200
|
+
if ('error' in target)
|
|
201
|
+
return target.error;
|
|
202
|
+
const id = c.req.param('id');
|
|
203
|
+
const { databaseAdapter } = c.var.aphexCMS;
|
|
204
|
+
const existing = await databaseAdapter.getJob(target.organizationId, id);
|
|
205
|
+
if (!existing)
|
|
206
|
+
return c.json({ success: false, error: 'Job not found' }, 404);
|
|
207
|
+
// `cancelJob` itself is unguarded — the scheduled-publish flow calls it on jobs it
|
|
208
|
+
// knows are pending. Here the state has to be checked first: cancelling a `leased`
|
|
209
|
+
// job would only look like it worked, because the worker holding it settles the row
|
|
210
|
+
// afterwards and overwrites the status.
|
|
211
|
+
if (existing.status !== 'pending' && existing.status !== 'failed') {
|
|
212
|
+
return c.json({
|
|
213
|
+
success: false,
|
|
214
|
+
error: `Only pending or failed jobs can be cancelled — this one is ${existing.status}.`
|
|
215
|
+
}, 409);
|
|
216
|
+
}
|
|
217
|
+
await databaseAdapter.cancelJob(target.organizationId, id);
|
|
218
|
+
const job = await databaseAdapter.getJob(target.organizationId, id);
|
|
219
|
+
cmsLogger.info(`[jobs] Job ${id} (${existing.type}) cancelled by ${gate.auth.type === 'session' ? gate.auth.user.id : 'api key'}`);
|
|
220
|
+
return c.json({ success: true, data: job });
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
cmsLogger.error('Failed to cancel job:', error);
|
|
224
|
+
return c.json({ success: false, error: 'Failed to cancel job' }, 500);
|
|
225
|
+
}
|
|
69
226
|
})
|
|
70
227
|
.get('/events', async (c) => {
|
|
71
228
|
try {
|
|
@@ -76,15 +233,12 @@ export const jobsRouter = new Hono()
|
|
|
76
233
|
if (!parsed.success) {
|
|
77
234
|
return c.json({ success: false, error: 'Invalid query', issues: parsed.error.issues }, 400);
|
|
78
235
|
}
|
|
79
|
-
const { type, limit, offset } = parsed.data;
|
|
236
|
+
const { type, scope, limit, offset } = parsed.data;
|
|
237
|
+
const organizationId = readScope(gate.auth, gate.organizationId, scope);
|
|
80
238
|
const { databaseAdapter, auth } = c.var.aphexCMS;
|
|
81
|
-
const page = await databaseAdapter.listEvents({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
limit,
|
|
85
|
-
offset
|
|
86
|
-
});
|
|
87
|
-
const items = await withCreatedByNames(page.items, auth);
|
|
239
|
+
const page = await databaseAdapter.listEvents({ organizationId, type, limit, offset });
|
|
240
|
+
const named = await withCreatedByNames(page.items, auth);
|
|
241
|
+
const items = organizationId ? named : await withOrganizationNames(named, databaseAdapter);
|
|
88
242
|
return c.json({ success: true, data: items, pagination: toPagination(page) });
|
|
89
243
|
}
|
|
90
244
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations-by-id.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/organizations-by-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"organizations-by-id.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/organizations-by-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAkDzC,eAAO,MAAM,uBAAuB,EAAE,IAAI,CAAC,QAAQ,CAiQhD,CAAC"}
|
|
@@ -2,7 +2,42 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { zValidator } from '@hono/zod-validator';
|
|
3
3
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
4
4
|
import { updateOrganizationRequest } from '../../../api/schemas/organizations.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isInstanceRole } from '../../../types/capabilities.js';
|
|
6
|
+
/** How many assets to pull per page while erasing an organization's media. */
|
|
7
|
+
const ASSET_ERASE_PAGE_SIZE = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Delete every asset in an organization — object storage included.
|
|
10
|
+
*
|
|
11
|
+
* Paged rather than loaded at once so a large media library doesn't have to fit in
|
|
12
|
+
* memory. Always reads page zero: each pass deletes what it read, so the next unerased
|
|
13
|
+
* asset is always at the front. Best-effort per asset — one failed object shouldn't
|
|
14
|
+
* abandon the rest, and the caller's retry will pick up whatever is left.
|
|
15
|
+
*/
|
|
16
|
+
async function eraseOrganizationAssets(assetService, organizationId) {
|
|
17
|
+
for (;;) {
|
|
18
|
+
const page = await assetService.findAssets(organizationId, {
|
|
19
|
+
limit: ASSET_ERASE_PAGE_SIZE,
|
|
20
|
+
offset: 0
|
|
21
|
+
});
|
|
22
|
+
if (page.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
let erased = 0;
|
|
25
|
+
for (const asset of page) {
|
|
26
|
+
try {
|
|
27
|
+
await assetService.deleteAsset(organizationId, asset.id);
|
|
28
|
+
erased++;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
cmsLogger.warn(`Could not erase asset ${asset.id} while deleting org:`, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Nothing went away, so paging again would loop forever on the same failures.
|
|
35
|
+
if (erased === 0) {
|
|
36
|
+
cmsLogger.error(`Stopped erasing assets for org ${organizationId}: ${page.length} remain and none could be deleted.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
6
41
|
export const organizationsByIdRouter = new Hono()
|
|
7
42
|
.get('/:id', async (c) => {
|
|
8
43
|
try {
|
|
@@ -56,7 +91,7 @@ export const organizationsByIdRouter = new Hono()
|
|
|
56
91
|
}
|
|
57
92
|
}), async (c) => {
|
|
58
93
|
try {
|
|
59
|
-
const { databaseAdapter } = c.var.aphexCMS;
|
|
94
|
+
const { databaseAdapter, rolesService } = c.var.aphexCMS;
|
|
60
95
|
const auth = c.var.auth;
|
|
61
96
|
const id = c.req.param('id');
|
|
62
97
|
if (!auth || auth.type !== 'session') {
|
|
@@ -74,7 +109,17 @@ export const organizationsByIdRouter = new Hono()
|
|
|
74
109
|
}, 400);
|
|
75
110
|
}
|
|
76
111
|
const membership = await databaseAdapter.findUserMembership(auth.user.id, id);
|
|
77
|
-
|
|
112
|
+
// Capabilities have to be evaluated against the *target* organization.
|
|
113
|
+
// `auth.capabilities` is resolved once per request for the caller's
|
|
114
|
+
// **active** org, so checking it here let an admin in org A who is only a
|
|
115
|
+
// viewer in org B edit B's settings simply by leaving A active.
|
|
116
|
+
// Instance roles still override — they hold every capability everywhere.
|
|
117
|
+
let canEditSettings = isInstanceRole(auth);
|
|
118
|
+
if (membership && !canEditSettings) {
|
|
119
|
+
const targetCapabilities = await rolesService.getCapabilities(id, membership.role);
|
|
120
|
+
canEditSettings = targetCapabilities.includes('org.settings');
|
|
121
|
+
}
|
|
122
|
+
if (!membership || !canEditSettings) {
|
|
78
123
|
return c.json({
|
|
79
124
|
success: false,
|
|
80
125
|
error: 'Forbidden',
|
|
@@ -155,6 +200,19 @@ export const organizationsByIdRouter = new Hono()
|
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
202
|
}
|
|
203
|
+
// Files first, while the rows that point at them still exist. Deleting the
|
|
204
|
+
// organization cascades its `cms_assets` rows away, and those rows hold the
|
|
205
|
+
// only record of each object's storage path — so anything not erased here
|
|
206
|
+
// stays in the bucket forever, unreferenced and unfindable, still publicly
|
|
207
|
+
// readable by URL. `assetService.deleteAsset` removes the object *and* the
|
|
208
|
+
// row; the cascade then has less to do.
|
|
209
|
+
//
|
|
210
|
+
// Synchronous and unbounded, which is the known limit: an organization with
|
|
211
|
+
// tens of thousands of assets will make this request slow. The durable fix is
|
|
212
|
+
// to drain them through the job queue, which needs a deletion state machine
|
|
213
|
+
// (the org has to stay readable while its assets are erased) — deliberately
|
|
214
|
+
// not built here. It is safe to retry: an already-erased asset is a no-op.
|
|
215
|
+
await eraseOrganizationAssets(c.var.aphexCMS.assetService, id);
|
|
158
216
|
await databaseAdapter.removeAllMembers(id);
|
|
159
217
|
await databaseAdapter.removeAllInvitations(id);
|
|
160
218
|
await databaseAdapter.deleteOrganization(id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations-invitations.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/organizations-invitations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"organizations-invitations.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/organizations-invitations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAU5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,EAAE,IAAI,CAAC,QAAQ,CA4OxD,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { zValidator } from '@hono/zod-validator';
|
|
|
3
3
|
import { cmsLogger } from '../../../utils/logger.js';
|
|
4
4
|
import { inviteMemberRequest, cancelInvitationRequest } from '../../../api/schemas/organizations.js';
|
|
5
5
|
import { hasCapability } from '../../../types/capabilities.js';
|
|
6
|
+
import { isPendingInvitation, isStaleInvitation, invitationExpiryFrom } from '../../../auth/invitation-status.js';
|
|
6
7
|
/**
|
|
7
8
|
* Note: in studio, invitations are wrapped by a SvelteKit `+server.ts`
|
|
8
9
|
* that adds email sending after the invite row is created. While that
|
|
@@ -38,6 +39,18 @@ export const organizationsInvitationsRouter = new Hono()
|
|
|
38
39
|
}, 403);
|
|
39
40
|
}
|
|
40
41
|
const body = c.req.valid('json');
|
|
42
|
+
// The same restriction the member role-change route enforces, and for
|
|
43
|
+
// the same reason: `member.changeRole` refuses to promote anyone to
|
|
44
|
+
// owner, so admins must not be able to reach the same place by inviting
|
|
45
|
+
// a brand-new one instead — that detour would let an admin seat an
|
|
46
|
+
// accomplice above the actual owner.
|
|
47
|
+
if (body.role === 'owner' && auth.organizationRole !== 'owner') {
|
|
48
|
+
return c.json({
|
|
49
|
+
success: false,
|
|
50
|
+
error: 'Forbidden',
|
|
51
|
+
message: 'Only owners can invite members as owner'
|
|
52
|
+
}, 403);
|
|
53
|
+
}
|
|
41
54
|
const roleRow = await databaseAdapter.findRoleByName(auth.organizationId, body.role);
|
|
42
55
|
if (!roleRow) {
|
|
43
56
|
return c.json({
|
|
@@ -67,7 +80,10 @@ export const organizationsInvitationsRouter = new Hono()
|
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
const existingInvitations = await databaseAdapter.findOrganizationInvitations(auth.organizationId);
|
|
70
|
-
const
|
|
83
|
+
const sameEmail = existingInvitations.filter((inv) => inv.email.toLowerCase() === body.email.toLowerCase());
|
|
84
|
+
// Arrow, not point-free: `.find(isPendingInvitation)` would pass the array
|
|
85
|
+
// index as the `now` argument.
|
|
86
|
+
const pendingInvitation = sameEmail.find((inv) => isPendingInvitation(inv));
|
|
71
87
|
if (pendingInvitation) {
|
|
72
88
|
return c.json({
|
|
73
89
|
success: false,
|
|
@@ -75,6 +91,19 @@ export const organizationsInvitationsRouter = new Hono()
|
|
|
75
91
|
message: 'This email has already been invited to the organization'
|
|
76
92
|
}, 400);
|
|
77
93
|
}
|
|
94
|
+
// Clear lapsed invitations for this address so re-inviting replaces rather
|
|
95
|
+
// than accumulates. `isStaleInvitation` deliberately spares accepted ones —
|
|
96
|
+
// those are the record that somebody joined by invitation. Best-effort: a
|
|
97
|
+
// stale row is inert to every other path, so failing to tidy up shouldn't
|
|
98
|
+
// block the invitation actually being asked for.
|
|
99
|
+
for (const stale of sameEmail.filter((inv) => isStaleInvitation(inv))) {
|
|
100
|
+
try {
|
|
101
|
+
await databaseAdapter.deleteInvitation(stale.id, auth.organizationId);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
cmsLogger.warn('Failed to clear expired invitation:', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
78
107
|
const token = crypto.randomUUID();
|
|
79
108
|
const invitation = await databaseAdapter.createInvitation({
|
|
80
109
|
organizationId: auth.organizationId,
|
|
@@ -82,7 +111,7 @@ export const organizationsInvitationsRouter = new Hono()
|
|
|
82
111
|
role: body.role,
|
|
83
112
|
invitedBy: auth.user.id,
|
|
84
113
|
token,
|
|
85
|
-
expiresAt:
|
|
114
|
+
expiresAt: invitationExpiryFrom()
|
|
86
115
|
});
|
|
87
116
|
return c.json({
|
|
88
117
|
success: true,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../../../db/interfaces/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `organizationId` on a list of rows to the organization's name.
|
|
4
|
+
*
|
|
5
|
+
* Only needed by the instance-wide history views, where rows from several tenants sit in one
|
|
6
|
+
* table and a bare uuid tells a super admin nothing about which customer is affected. The
|
|
7
|
+
* org-scoped views skip this entirely — every row belongs to the org you're already in.
|
|
8
|
+
*
|
|
9
|
+
* Lookups are deduped and run in parallel; an org that can't be read resolves to `null`
|
|
10
|
+
* rather than failing the page, since a missing name is cosmetic and a 500 is not.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withOrganizationNames<T extends {
|
|
13
|
+
organizationId: string;
|
|
14
|
+
}>(rows: T[], db: DatabaseAdapter): Promise<Array<T & {
|
|
15
|
+
organizationName: string | null;
|
|
16
|
+
}>>;
|
|
17
|
+
//# sourceMappingURL=resolve-organization-names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-organization-names.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/resolve-organization-names.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,SAAS;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,EAC/E,IAAI,EAAE,CAAC,EAAE,EACT,EAAE,EAAE,eAAe,GACjB,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG;IAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,CAgBzD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `organizationId` on a list of rows to the organization's name.
|
|
3
|
+
*
|
|
4
|
+
* Only needed by the instance-wide history views, where rows from several tenants sit in one
|
|
5
|
+
* table and a bare uuid tells a super admin nothing about which customer is affected. The
|
|
6
|
+
* org-scoped views skip this entirely — every row belongs to the org you're already in.
|
|
7
|
+
*
|
|
8
|
+
* Lookups are deduped and run in parallel; an org that can't be read resolves to `null`
|
|
9
|
+
* rather than failing the page, since a missing name is cosmetic and a 500 is not.
|
|
10
|
+
*/
|
|
11
|
+
export async function withOrganizationNames(rows, db) {
|
|
12
|
+
const ids = [...new Set(rows.map((r) => r.organizationId))];
|
|
13
|
+
const names = new Map();
|
|
14
|
+
await Promise.all(ids.map(async (id) => {
|
|
15
|
+
try {
|
|
16
|
+
const org = await db.findOrganizationById(id);
|
|
17
|
+
if (org)
|
|
18
|
+
names.set(id, org.name);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Unreadable org — leave the name null and show the id.
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
return rows.map((r) => ({ ...r, organizationName: names.get(r.organizationId) ?? null }));
|
|
25
|
+
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import type { AphexEnv } from '../index.js';
|
|
3
|
-
/**
|
|
4
|
-
* User account routes that delegate to the configured AuthProvider.
|
|
5
|
-
*
|
|
6
|
-
* cms-core ships these as Hono routers so studio doesn't need to maintain
|
|
7
|
-
* SvelteKit `+server.ts` files for them. The wire format and side effects
|
|
8
|
-
* (e.g. the password-reset email) are owned by the AuthProvider impl —
|
|
9
|
-
* cms-core's role is just to expose them over HTTP.
|
|
10
|
-
*/
|
|
11
3
|
export declare const userRouter: Hono<AphexEnv>;
|
|
12
4
|
//# sourceMappingURL=user.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/api/routes/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAoJzC,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,QAAQ,CAmNpC,CAAC"}
|