@shipfox/api-workspaces 10.1.0 → 11.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +7 -1
- package/dist/core/admin-workspaces.d.ts +16 -0
- package/dist/core/admin-workspaces.d.ts.map +1 -1
- package/dist/core/admin-workspaces.js +52 -0
- package/dist/core/admin-workspaces.js.map +1 -1
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +24 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/invitations.d.ts.map +1 -1
- package/dist/core/invitations.js +6 -3
- package/dist/core/invitations.js.map +1 -1
- package/dist/core/workspaces.d.ts +1 -0
- package/dist/core/workspaces.d.ts.map +1 -1
- package/dist/core/workspaces.js +5 -3
- package/dist/core/workspaces.js.map +1 -1
- package/dist/db/admin-workspace-commands.d.ts +17 -0
- package/dist/db/admin-workspace-commands.d.ts.map +1 -0
- package/dist/db/admin-workspace-commands.js +86 -0
- package/dist/db/admin-workspace-commands.js.map +1 -0
- package/dist/db/db.d.ts +256 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +135 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -0
- package/dist/db/schema/admin-command-results.js +18 -0
- package/dist/db/schema/admin-command-results.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +3 -2
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/admin-workspaces.d.ts.map +1 -1
- package/dist/presentation/routes/admin-workspaces.js +119 -14
- package/dist/presentation/routes/admin-workspaces.js.map +1 -1
- package/dist/presentation/routes/invitations/create.d.ts.map +1 -1
- package/dist/presentation/routes/invitations/create.js +5 -1
- package/dist/presentation/routes/invitations/create.js.map +1 -1
- package/dist/presentation/routes/invitations/list.d.ts.map +1 -1
- package/dist/presentation/routes/invitations/list.js +5 -1
- package/dist/presentation/routes/invitations/list.js.map +1 -1
- package/dist/presentation/routes/invitations/revoke.d.ts.map +1 -1
- package/dist/presentation/routes/invitations/revoke.js +5 -1
- package/dist/presentation/routes/invitations/revoke.js.map +1 -1
- package/dist/presentation/routes/members/list.d.ts.map +1 -1
- package/dist/presentation/routes/members/list.js +5 -1
- package/dist/presentation/routes/members/list.js.map +1 -1
- package/dist/presentation/routes/members/remove.d.ts.map +1 -1
- package/dist/presentation/routes/members/remove.js +5 -1
- package/dist/presentation/routes/members/remove.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_wide_enchantress.sql +11 -0
- package/drizzle/meta/0002_snapshot.json +555 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/core/admin-workspaces.ts +102 -0
- package/src/core/errors.ts +28 -0
- package/src/core/index.ts +8 -0
- package/src/core/invitations.test.ts +8 -8
- package/src/core/invitations.ts +3 -0
- package/src/core/workspaces.test.ts +22 -3
- package/src/core/workspaces.ts +4 -1
- package/src/db/admin-workspace-commands.ts +169 -0
- package/src/db/db.ts +2 -0
- package/src/db/index.ts +1 -0
- package/src/db/schema/admin-command-results.ts +30 -0
- package/src/index.test.ts +3 -1
- package/src/index.ts +17 -2
- package/src/presentation/inter-module.test.ts +51 -0
- package/src/presentation/inter-module.ts +7 -4
- package/src/presentation/routes/admin-workspaces.test.ts +273 -3
- package/src/presentation/routes/admin-workspaces.ts +143 -13
- package/src/presentation/routes/invitations/create.test.ts +15 -0
- package/src/presentation/routes/invitations/create.ts +2 -1
- package/src/presentation/routes/invitations/list.test.ts +30 -0
- package/src/presentation/routes/invitations/list.ts +2 -1
- package/src/presentation/routes/invitations/revoke.test.ts +14 -0
- package/src/presentation/routes/invitations/revoke.ts +2 -1
- package/src/presentation/routes/members/list.test.ts +29 -1
- package/src/presentation/routes/members/list.ts +2 -1
- package/src/presentation/routes/members/remove.test.ts +14 -0
- package/src/presentation/routes/members/remove.ts +2 -1
- package/test/globalSetup.ts +3 -1
- package/test/routes.ts +8 -3
- package/tsconfig.build.tsbuildinfo +1 -1
- package/vitest.config.ts +3 -0
|
@@ -82,6 +82,20 @@ describe('DELETE /workspaces/:workspaceId/invitations/:invitationId', () => {
|
|
|
82
82
|
expect(res.json().code).toBe('forbidden');
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test('returns workspace-suspended for a suspended membership claim', async () => {
|
|
86
|
+
const workspaceId = crypto.randomUUID();
|
|
87
|
+
const token = `claim:${crypto.randomUUID()}:suspended-invite-revoke@example.com:${workspaceId}:suspended`;
|
|
88
|
+
|
|
89
|
+
const res = await app.inject({
|
|
90
|
+
method: 'DELETE',
|
|
91
|
+
url: `/workspaces/${workspaceId}/invitations/${crypto.randomUUID()}`,
|
|
92
|
+
headers: {authorization: `Bearer ${token}`},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(res.statusCode).toBe(409);
|
|
96
|
+
expect(res.json().code).toBe('workspace-suspended');
|
|
97
|
+
});
|
|
98
|
+
|
|
85
99
|
test('transforms missing membership into 403', async () => {
|
|
86
100
|
const outsider = await signupVerifyLogin(app, 'invite-revoke-outsider');
|
|
87
101
|
const workspaceId = crypto.randomUUID();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {AUTH_USER, getUserContext} from '@shipfox/api-auth-context';
|
|
1
|
+
import {AUTH_USER, getUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
|
|
2
2
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
3
|
import {z} from 'zod';
|
|
4
4
|
import {
|
|
@@ -51,6 +51,7 @@ export const revokeInvitationRoute = defineRoute({
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const {workspaceId, invitationId} = request.params;
|
|
54
|
+
requireWorkspaceAccess({request, workspaceId});
|
|
54
55
|
|
|
55
56
|
await revokeWorkspaceInvitation({
|
|
56
57
|
workspaceId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {FastifyInstance} from 'fastify';
|
|
2
|
-
import {createWorkspace as createWorkspaceRow} from '#db/workspaces.js';
|
|
2
|
+
import {createWorkspace as createWorkspaceRow, updateWorkspace} from '#db/workspaces.js';
|
|
3
3
|
import {
|
|
4
4
|
createInvite,
|
|
5
5
|
createWorkspace,
|
|
@@ -50,6 +50,20 @@ describe('GET /workspaces/:workspaceId/members', () => {
|
|
|
50
50
|
).toEqual([guest.email, owner.email].sort());
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test('keeps an active claim usable when the workspace row is suspended', async () => {
|
|
54
|
+
const owner = await signupVerifyLogin(app, 'members-list-snapshot');
|
|
55
|
+
const workspaceId = await createWorkspace(app, owner.token);
|
|
56
|
+
await updateWorkspace({id: workspaceId, status: 'suspended'});
|
|
57
|
+
|
|
58
|
+
const res = await app.inject({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
url: `/workspaces/${workspaceId}/members`,
|
|
61
|
+
headers: {authorization: `Bearer ${owner.token}`},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(res.statusCode).toBe(200);
|
|
65
|
+
});
|
|
66
|
+
|
|
53
67
|
test('authorizes from a static membership claim without a membership row', async () => {
|
|
54
68
|
const userId = crypto.randomUUID();
|
|
55
69
|
const email = `members-list-claim-${crypto.randomUUID()}@example.com`;
|
|
@@ -66,6 +80,20 @@ describe('GET /workspaces/:workspaceId/members', () => {
|
|
|
66
80
|
expect(res.json().members).toEqual([]);
|
|
67
81
|
});
|
|
68
82
|
|
|
83
|
+
test('returns workspace-suspended for a suspended membership claim', async () => {
|
|
84
|
+
const workspaceId = crypto.randomUUID();
|
|
85
|
+
const token = `claim:${crypto.randomUUID()}:suspended-list@example.com:${workspaceId}:suspended`;
|
|
86
|
+
|
|
87
|
+
const res = await app.inject({
|
|
88
|
+
method: 'GET',
|
|
89
|
+
url: `/workspaces/${workspaceId}/members`,
|
|
90
|
+
headers: {authorization: `Bearer ${token}`},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(res.statusCode).toBe(409);
|
|
94
|
+
expect(res.json().code).toBe('workspace-suspended');
|
|
95
|
+
});
|
|
96
|
+
|
|
69
97
|
test('transforms missing membership into 403', async () => {
|
|
70
98
|
const outsider = await signupVerifyLogin(app, 'members-list-outsider');
|
|
71
99
|
const workspaceId = crypto.randomUUID();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {AUTH_USER, getUserContext} from '@shipfox/api-auth-context';
|
|
1
|
+
import {AUTH_USER, getUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
|
|
2
2
|
import {listMembersResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
3
3
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
4
4
|
import {z} from 'zod';
|
|
@@ -40,6 +40,7 @@ export const listMembersRoute = defineRoute({
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const {workspaceId} = request.params;
|
|
43
|
+
requireWorkspaceAccess({request, workspaceId});
|
|
43
44
|
const members = await listWorkspaceMembers({
|
|
44
45
|
workspaceId,
|
|
45
46
|
requesterUserId: client.userId,
|
|
@@ -79,6 +79,20 @@ describe('DELETE /workspaces/:workspaceId/members/:userId', () => {
|
|
|
79
79
|
expect(res.json().code).toBe('self-removal-not-allowed');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('returns workspace-suspended for a suspended membership claim', async () => {
|
|
83
|
+
const workspaceId = crypto.randomUUID();
|
|
84
|
+
const token = `claim:${crypto.randomUUID()}:suspended-member-remove@example.com:${workspaceId}:suspended`;
|
|
85
|
+
|
|
86
|
+
const res = await app.inject({
|
|
87
|
+
method: 'DELETE',
|
|
88
|
+
url: `/workspaces/${workspaceId}/members/${crypto.randomUUID()}`,
|
|
89
|
+
headers: {authorization: `Bearer ${token}`},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(res.statusCode).toBe(409);
|
|
93
|
+
expect(res.json().code).toBe('workspace-suspended');
|
|
94
|
+
});
|
|
95
|
+
|
|
82
96
|
test('transforms missing membership into 403', async () => {
|
|
83
97
|
const outsider = await signupVerifyLogin(app, 'members-remove-outsider');
|
|
84
98
|
const workspaceId = crypto.randomUUID();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {AUTH_USER, getUserContext} from '@shipfox/api-auth-context';
|
|
1
|
+
import {AUTH_USER, getUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
|
|
2
2
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
3
3
|
import {z} from 'zod';
|
|
4
4
|
import {
|
|
@@ -53,6 +53,7 @@ export const removeMemberRoute = defineRoute({
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const {workspaceId, userId} = request.params;
|
|
56
|
+
requireWorkspaceAccess({request, workspaceId});
|
|
56
57
|
|
|
57
58
|
await removeWorkspaceMember({
|
|
58
59
|
workspaceId,
|
package/test/globalSetup.ts
CHANGED
|
@@ -8,7 +8,9 @@ export async function setup() {
|
|
|
8
8
|
createPostgresClient();
|
|
9
9
|
|
|
10
10
|
await runMigrations(db(), migrationsPath, '__drizzle_migrations_workspaces');
|
|
11
|
-
await db().execute(
|
|
11
|
+
await db().execute(
|
|
12
|
+
sql`TRUNCATE workspaces_admin_command_results, workspaces_outbox, workspaces_workspaces CASCADE`,
|
|
13
|
+
);
|
|
12
14
|
|
|
13
15
|
closeDb();
|
|
14
16
|
await closePostgresClient();
|
package/test/routes.ts
CHANGED
|
@@ -52,14 +52,15 @@ const fakeUserAuth: AuthMethod = {
|
|
|
52
52
|
authenticate: async (request) => {
|
|
53
53
|
const raw = request.headers.authorization?.replace(BEARER_RE, '');
|
|
54
54
|
if (raw?.startsWith('claim:')) {
|
|
55
|
-
const [, userId, email, workspaceId] = raw.split(':');
|
|
55
|
+
const [, userId, email, workspaceId, status] = raw.split(':');
|
|
56
56
|
if (!userId || !email || !workspaceId) throw new Error('Invalid test user claim token');
|
|
57
|
+
const workspaceStatus = status === 'suspended' || status === 'deleted' ? status : 'active';
|
|
57
58
|
setUserContext(
|
|
58
59
|
request,
|
|
59
60
|
buildUserContext({
|
|
60
61
|
userId,
|
|
61
62
|
email,
|
|
62
|
-
memberships: [{workspaceId, role: 'admin'}],
|
|
63
|
+
memberships: [{workspaceId, role: 'admin', workspaceStatus}],
|
|
63
64
|
}),
|
|
64
65
|
);
|
|
65
66
|
return;
|
|
@@ -74,7 +75,11 @@ const fakeUserAuth: AuthMethod = {
|
|
|
74
75
|
userId,
|
|
75
76
|
email,
|
|
76
77
|
name: encodedName ? decodeURIComponent(encodedName) : null,
|
|
77
|
-
memberships: memberships.map((m) => ({
|
|
78
|
+
memberships: memberships.map((m) => ({
|
|
79
|
+
workspaceId: m.workspaceId,
|
|
80
|
+
role: 'admin' as const,
|
|
81
|
+
workspaceStatus: 'active' as const,
|
|
82
|
+
})),
|
|
78
83
|
}),
|
|
79
84
|
);
|
|
80
85
|
},
|