@actuate-media/cms-core 0.39.0 → 0.41.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/__tests__/api/users-detail.test.d.ts +2 -0
- package/dist/__tests__/api/users-detail.test.d.ts.map +1 -0
- package/dist/__tests__/api/users-detail.test.js +401 -0
- package/dist/__tests__/api/users-detail.test.js.map +1 -0
- package/dist/__tests__/api/users-invite.test.js +80 -26
- package/dist/__tests__/api/users-invite.test.js.map +1 -1
- package/dist/__tests__/auth/invites.test.d.ts +2 -0
- package/dist/__tests__/auth/invites.test.d.ts.map +1 -0
- package/dist/__tests__/auth/invites.test.js +181 -0
- package/dist/__tests__/auth/invites.test.js.map +1 -0
- package/dist/__tests__/auth/roles.test.d.ts +2 -0
- package/dist/__tests__/auth/roles.test.d.ts.map +1 -0
- package/dist/__tests__/auth/roles.test.js +60 -0
- package/dist/__tests__/auth/roles.test.js.map +1 -0
- package/dist/__tests__/auth/team.test.d.ts +2 -0
- package/dist/__tests__/auth/team.test.d.ts.map +1 -0
- package/dist/__tests__/auth/team.test.js +139 -0
- package/dist/__tests__/auth/team.test.js.map +1 -0
- package/dist/__tests__/auth/user-admin.test.d.ts +2 -0
- package/dist/__tests__/auth/user-admin.test.d.ts.map +1 -0
- package/dist/__tests__/auth/user-admin.test.js +198 -0
- package/dist/__tests__/auth/user-admin.test.js.map +1 -0
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +883 -16
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/invites.d.ts +71 -0
- package/dist/auth/invites.d.ts.map +1 -0
- package/dist/auth/invites.js +279 -0
- package/dist/auth/invites.js.map +1 -0
- package/dist/auth/reset.d.ts.map +1 -1
- package/dist/auth/reset.js +27 -4
- package/dist/auth/reset.js.map +1 -1
- package/dist/auth/roles.d.ts +51 -0
- package/dist/auth/roles.d.ts.map +1 -0
- package/dist/auth/roles.js +159 -0
- package/dist/auth/roles.js.map +1 -0
- package/dist/auth/team.d.ts +86 -0
- package/dist/auth/team.d.ts.map +1 -0
- package/dist/auth/team.js +270 -0
- package/dist/auth/team.js.map +1 -0
- package/dist/auth/user-admin.d.ts +118 -0
- package/dist/auth/user-admin.d.ts.map +1 -0
- package/dist/auth/user-admin.js +292 -0
- package/dist/auth/user-admin.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/prisma/migrations/0011_user_roles_owner_viewer/migration.sql +6 -0
- package/prisma/migrations/0012_invites_and_user_lifecycle/migration.sql +40 -0
- package/prisma/schema.prisma +43 -0
- package/dist/__tests__/auth/invite.test.d.ts +0 -2
- package/dist/__tests__/auth/invite.test.d.ts.map +0 -1
- package/dist/__tests__/auth/invite.test.js +0 -151
- package/dist/__tests__/auth/invite.test.js.map +0 -1
- package/dist/auth/invite.d.ts +0 -37
- package/dist/auth/invite.d.ts.map +0 -1
- package/dist/auth/invite.js +0 -104
- package/dist/auth/invite.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users-detail.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/users-detail.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { createSession } from '../../auth/session.js';
|
|
4
|
+
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
|
|
5
|
+
import { initDB } from '../../db.js';
|
|
6
|
+
import { setEmailSender } from '../../email/sender.js';
|
|
7
|
+
import { setActuateConfig } from '../../config/runtime.js';
|
|
8
|
+
/**
|
|
9
|
+
* Integration tests for the User detail drawer + security action routes:
|
|
10
|
+
* GET /users/:id, sessions list/revoke/revoke-all, permissions, suspend,
|
|
11
|
+
* reactivate, lock/unlock, force/send password reset, require-mfa, mfa-reset.
|
|
12
|
+
*
|
|
13
|
+
* Focus: response contracts + the access-control guards (admin-only, Owner-only
|
|
14
|
+
* on Owners, last-Owner protection, self-action blocks, reauth on MFA reset).
|
|
15
|
+
*/
|
|
16
|
+
const SECRET = 'test-secret-for-users-detail-routes-1234567890abc';
|
|
17
|
+
function createMockDB(userRows, sessionRows = []) {
|
|
18
|
+
const users = userRows.map((r) => ({ isActive: true, passwordHash: 'h', ...r }));
|
|
19
|
+
const sessions = [...sessionRows];
|
|
20
|
+
const audit = [];
|
|
21
|
+
const resetTokens = [];
|
|
22
|
+
return {
|
|
23
|
+
_users: users,
|
|
24
|
+
_sessions: sessions,
|
|
25
|
+
_audit: audit,
|
|
26
|
+
_resetTokens: resetTokens,
|
|
27
|
+
user: {
|
|
28
|
+
findMany: async () => users,
|
|
29
|
+
findUnique: async ({ where }) => users.find((u) => u.id === where.id) ?? null,
|
|
30
|
+
findFirst: async ({ where }) => {
|
|
31
|
+
if (where?.email?.equals) {
|
|
32
|
+
const t = String(where.email.equals).toLowerCase();
|
|
33
|
+
return users.find((u) => u.email.toLowerCase() === t) ?? null;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
count: async ({ where } = {}) => users.filter((u) => (where?.role ? u.role === where.role : true) &&
|
|
38
|
+
(where?.isActive !== undefined ? (u.isActive ?? true) === where.isActive : true) &&
|
|
39
|
+
(where?.id?.not ? u.id !== where.id.not : true)).length,
|
|
40
|
+
update: async ({ where, data }) => {
|
|
41
|
+
const u = users.find((x) => x.id === where.id);
|
|
42
|
+
Object.assign(u, data);
|
|
43
|
+
return u;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
session: {
|
|
47
|
+
findMany: async ({ where } = {}) => sessions.filter((s) => (where?.userId ? s.userId === where.userId : true)),
|
|
48
|
+
findUnique: async ({ where }) => {
|
|
49
|
+
const found = sessions.find((s) => s.id === where.id);
|
|
50
|
+
if (found)
|
|
51
|
+
return found;
|
|
52
|
+
// Auth session lookup performed by extractSession — the JWT carries a
|
|
53
|
+
// `session-<userId>` id that has no corresponding row in these tests.
|
|
54
|
+
// Treat it as a valid, non-revoked session so auth succeeds.
|
|
55
|
+
if (String(where.id).startsWith('session-')) {
|
|
56
|
+
return { id: where.id, revokedAt: null, expiresAt: new Date(Date.now() + 1e8) };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
},
|
|
60
|
+
update: async ({ where, data }) => {
|
|
61
|
+
const s = sessions.find((x) => x.id === where.id);
|
|
62
|
+
Object.assign(s, data);
|
|
63
|
+
return s;
|
|
64
|
+
},
|
|
65
|
+
updateMany: async ({ where, data }) => {
|
|
66
|
+
let n = 0;
|
|
67
|
+
for (const s of sessions) {
|
|
68
|
+
const matchUser = where?.userId ? s.userId === where.userId : true;
|
|
69
|
+
const matchRevoked = where?.revokedAt === null ? s.revokedAt === null : true;
|
|
70
|
+
const matchNot = where?.id?.not ? s.id !== where.id.not : true;
|
|
71
|
+
if (matchUser && matchRevoked && matchNot) {
|
|
72
|
+
Object.assign(s, data);
|
|
73
|
+
n++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { count: n };
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
auditLog: {
|
|
80
|
+
findMany: async () => audit,
|
|
81
|
+
create: async ({ data }) => {
|
|
82
|
+
audit.push(data);
|
|
83
|
+
return data;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
oAuthAccount: { findMany: async () => [] },
|
|
87
|
+
document: {
|
|
88
|
+
count: async () => 0,
|
|
89
|
+
updateMany: async ({ where }) => {
|
|
90
|
+
// Reassign any docs "owned" by the from-user in this lightweight model.
|
|
91
|
+
const n = where?.createdById === 'u-owns' ? 2 : 0;
|
|
92
|
+
return { count: n };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
media: { count: async () => 0, updateMany: async () => ({ count: 0 }) },
|
|
96
|
+
contentTemplate: { count: async () => 0, updateMany: async () => ({ count: 0 }) },
|
|
97
|
+
passwordResetToken: {
|
|
98
|
+
updateMany: async () => ({ count: 0 }),
|
|
99
|
+
create: async ({ data }) => {
|
|
100
|
+
resetTokens.push(data);
|
|
101
|
+
return data;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function headersFor(userId, role) {
|
|
107
|
+
const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
|
|
108
|
+
const csrf = await generateCsrfToken();
|
|
109
|
+
return {
|
|
110
|
+
cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
|
|
111
|
+
'x-csrf-token': csrf,
|
|
112
|
+
'content-type': 'application/json',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const BASE = 'https://cms.example.com/api/cms';
|
|
116
|
+
describe('User detail + security action routes', () => {
|
|
117
|
+
const original = {
|
|
118
|
+
CMS_SECRET: process.env.CMS_SECRET,
|
|
119
|
+
CMS_SESSION_SECRET: process.env.CMS_SESSION_SECRET,
|
|
120
|
+
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
|
121
|
+
};
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
process.env.CMS_SECRET = SECRET;
|
|
124
|
+
delete process.env.CMS_SESSION_SECRET;
|
|
125
|
+
process.env.NEXT_PUBLIC_SITE_URL = 'https://cms.example.com';
|
|
126
|
+
setEmailSender(null);
|
|
127
|
+
// Pin a clean runtime config so a `platform.email` adapter leaked onto the
|
|
128
|
+
// global config by a previously-run test file can't make the admin
|
|
129
|
+
// password-reset flow await a foreign (possibly hanging) sender instead of
|
|
130
|
+
// the one registered via `setEmailSender`.
|
|
131
|
+
setActuateConfig({ admin: { path: '/admin' } });
|
|
132
|
+
});
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
setEmailSender(null);
|
|
135
|
+
for (const [k, v] of Object.entries(original)) {
|
|
136
|
+
if (v === undefined)
|
|
137
|
+
delete process.env[k];
|
|
138
|
+
else
|
|
139
|
+
process.env[k] = v;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
function makeHandler(users, sessions = []) {
|
|
143
|
+
const db = createMockDB(users, sessions);
|
|
144
|
+
initDB(db);
|
|
145
|
+
return { handler: handleActuateAPI({ prismaClient: db }), db };
|
|
146
|
+
}
|
|
147
|
+
const admin = () => ({
|
|
148
|
+
id: 'admin-1',
|
|
149
|
+
email: 'admin@a.com',
|
|
150
|
+
name: 'Admin',
|
|
151
|
+
role: 'ADMIN',
|
|
152
|
+
});
|
|
153
|
+
const owner = () => ({
|
|
154
|
+
id: 'owner-1',
|
|
155
|
+
email: 'owner@a.com',
|
|
156
|
+
name: 'Owner',
|
|
157
|
+
role: 'OWNER',
|
|
158
|
+
});
|
|
159
|
+
function req(path, headers, method = 'GET', body) {
|
|
160
|
+
return new Request(`${BASE}${path}`, {
|
|
161
|
+
method,
|
|
162
|
+
headers,
|
|
163
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
it('GET /users/:id returns the detail shape (admin-only)', async () => {
|
|
167
|
+
const { handler } = makeHandler([
|
|
168
|
+
admin(),
|
|
169
|
+
{ id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' },
|
|
170
|
+
]);
|
|
171
|
+
const res = await handler(req('/users/u2', await headersFor('admin-1', 'ADMIN')));
|
|
172
|
+
expect(res.status).toBe(200);
|
|
173
|
+
const body = (await res.json());
|
|
174
|
+
expect(body.data.id).toBe('u2');
|
|
175
|
+
expect(body.data.roleName).toBe('Editor');
|
|
176
|
+
expect(body.data.status).toBe('active');
|
|
177
|
+
});
|
|
178
|
+
it('rejects a non-admin (editor) with 403', async () => {
|
|
179
|
+
const { handler } = makeHandler([{ id: 'ed-1', email: 'e@a.com', name: 'Ed', role: 'EDITOR' }]);
|
|
180
|
+
const res = await handler(req('/users/ed-1', await headersFor('ed-1', 'EDITOR')));
|
|
181
|
+
expect(res.status).toBe(403);
|
|
182
|
+
});
|
|
183
|
+
it('GET /users/:id/sessions lists sessions with current marker', async () => {
|
|
184
|
+
const { handler } = makeHandler([admin()], [
|
|
185
|
+
{
|
|
186
|
+
id: 'session-admin-1',
|
|
187
|
+
userId: 'admin-1',
|
|
188
|
+
userAgent: 'Chrome',
|
|
189
|
+
expiresAt: new Date(Date.now() + 86400000),
|
|
190
|
+
createdAt: new Date(),
|
|
191
|
+
revokedAt: null,
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
const res = await handler(req('/users/admin-1/sessions', await headersFor('admin-1', 'ADMIN')));
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
const body = (await res.json());
|
|
197
|
+
expect(body.data[0].current).toBe(true);
|
|
198
|
+
expect(body.data[0].active).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
it('DELETE /users/:id/sessions/:sessionId revokes one session', async () => {
|
|
201
|
+
const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
|
|
202
|
+
{
|
|
203
|
+
id: 'sx',
|
|
204
|
+
userId: 'u2',
|
|
205
|
+
expiresAt: new Date(Date.now() + 86400000),
|
|
206
|
+
createdAt: new Date(),
|
|
207
|
+
revokedAt: null,
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
const res = await handler(req('/users/u2/sessions/sx', await headersFor('admin-1', 'ADMIN'), 'DELETE'));
|
|
211
|
+
expect(res.status).toBe(200);
|
|
212
|
+
expect(db._sessions[0].revokedAt).toBeInstanceOf(Date);
|
|
213
|
+
});
|
|
214
|
+
it('POST /users/:id/sessions/revoke-all revokes every active session for another user', async () => {
|
|
215
|
+
const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
|
|
216
|
+
{
|
|
217
|
+
id: 's1',
|
|
218
|
+
userId: 'u2',
|
|
219
|
+
expiresAt: new Date(Date.now() + 1e8),
|
|
220
|
+
createdAt: new Date(),
|
|
221
|
+
revokedAt: null,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 's2',
|
|
225
|
+
userId: 'u2',
|
|
226
|
+
expiresAt: new Date(Date.now() + 1e8),
|
|
227
|
+
createdAt: new Date(),
|
|
228
|
+
revokedAt: null,
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
const res = await handler(req('/users/u2/sessions/revoke-all', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
232
|
+
expect(res.status).toBe(200);
|
|
233
|
+
expect(db._sessions.every((s) => s.revokedAt instanceof Date)).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
it('GET /users/:id/permissions returns derived permissions', async () => {
|
|
236
|
+
const { handler } = makeHandler([
|
|
237
|
+
admin(),
|
|
238
|
+
{ id: 'u2', email: 'v@a.com', name: 'V', role: 'VIEWER' },
|
|
239
|
+
]);
|
|
240
|
+
const res = await handler(req('/users/u2/permissions', await headersFor('admin-1', 'ADMIN')));
|
|
241
|
+
const body = (await res.json());
|
|
242
|
+
expect(body.data.roleName).toBe('Viewer');
|
|
243
|
+
expect(body.data.permissions).toEqual(['dashboard.view', 'content.view']);
|
|
244
|
+
});
|
|
245
|
+
it('POST /users/:id/suspend suspends a user and revokes sessions', async () => {
|
|
246
|
+
const { handler, db } = makeHandler([admin(), { id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR' }], [
|
|
247
|
+
{
|
|
248
|
+
id: 's1',
|
|
249
|
+
userId: 'u2',
|
|
250
|
+
expiresAt: new Date(Date.now() + 1e8),
|
|
251
|
+
createdAt: new Date(),
|
|
252
|
+
revokedAt: null,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
const res = await handler(req('/users/u2/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
256
|
+
expect(res.status).toBe(200);
|
|
257
|
+
expect(db._users.find((u) => u.id === 'u2').suspendedAt).toBeInstanceOf(Date);
|
|
258
|
+
expect(db._sessions[0].revokedAt).toBeInstanceOf(Date);
|
|
259
|
+
});
|
|
260
|
+
it('blocks suspending your own account', async () => {
|
|
261
|
+
const { handler } = makeHandler([admin()]);
|
|
262
|
+
const res = await handler(req('/users/admin-1/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
263
|
+
expect(res.status).toBe(400);
|
|
264
|
+
});
|
|
265
|
+
it('protects the last Owner from suspension', async () => {
|
|
266
|
+
const { handler } = makeHandler([owner(), admin()]);
|
|
267
|
+
const res = await handler(req('/users/owner-1/suspend', await headersFor('owner-1', 'OWNER'), 'POST'));
|
|
268
|
+
// owner-1 is acting on themselves here → blocked as self-action first
|
|
269
|
+
expect(res.status).toBe(400);
|
|
270
|
+
});
|
|
271
|
+
it('forbids a non-Owner admin from acting on an Owner', async () => {
|
|
272
|
+
const { handler } = makeHandler([admin(), owner()]);
|
|
273
|
+
const res = await handler(req('/users/owner-1/suspend', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
274
|
+
expect(res.status).toBe(403);
|
|
275
|
+
});
|
|
276
|
+
it('blocks suspending the last Owner (acted on by another Owner)', async () => {
|
|
277
|
+
const { handler } = makeHandler([
|
|
278
|
+
owner(),
|
|
279
|
+
{ id: 'owner-2', email: 'o2@a.com', name: 'O2', role: 'OWNER', isActive: false },
|
|
280
|
+
]);
|
|
281
|
+
const res = await handler(req('/users/owner-1/suspend', await headersFor('owner-2', 'OWNER'), 'POST'));
|
|
282
|
+
// owner-2 is inactive but still the actor; owner-1 is the only active Owner.
|
|
283
|
+
expect(res.status).toBe(409);
|
|
284
|
+
});
|
|
285
|
+
it('reactivate clears suspension', async () => {
|
|
286
|
+
const { handler, db } = makeHandler([
|
|
287
|
+
admin(),
|
|
288
|
+
{ id: 'u2', email: 'p@a.com', name: 'Priya', role: 'EDITOR', suspendedAt: new Date() },
|
|
289
|
+
]);
|
|
290
|
+
const res = await handler(req('/users/u2/reactivate', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
291
|
+
expect(res.status).toBe(200);
|
|
292
|
+
expect(db._users.find((u) => u.id === 'u2').suspendedAt).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
it('force-password-reset sets the flag', async () => {
|
|
295
|
+
const { handler, db } = makeHandler([
|
|
296
|
+
admin(),
|
|
297
|
+
{ id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
|
|
298
|
+
]);
|
|
299
|
+
const res = await handler(req('/users/u2/force-password-reset', await headersFor('admin-1', 'ADMIN'), 'POST', {
|
|
300
|
+
enabled: true,
|
|
301
|
+
}));
|
|
302
|
+
expect(res.status).toBe(200);
|
|
303
|
+
expect(db._users.find((u) => u.id === 'u2').forcePasswordReset).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
it('require-mfa sets the flag', async () => {
|
|
306
|
+
const { handler, db } = makeHandler([
|
|
307
|
+
admin(),
|
|
308
|
+
{ id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
|
|
309
|
+
]);
|
|
310
|
+
const res = await handler(req('/users/u2/require-mfa', await headersFor('admin-1', 'ADMIN'), 'POST', {
|
|
311
|
+
required: true,
|
|
312
|
+
}));
|
|
313
|
+
expect(res.status).toBe(200);
|
|
314
|
+
expect(db._users.find((u) => u.id === 'u2').mfaRequired).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
it('mfa-reset requires reauthentication (401) when no password header is sent', async () => {
|
|
317
|
+
const { handler } = makeHandler([
|
|
318
|
+
admin(),
|
|
319
|
+
{ id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR', totpEnabled: true },
|
|
320
|
+
]);
|
|
321
|
+
const res = await handler(req('/users/u2/mfa-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
322
|
+
expect(res.status).toBe(401);
|
|
323
|
+
const body = (await res.json());
|
|
324
|
+
expect(body.code ?? body.error).toBeTruthy();
|
|
325
|
+
});
|
|
326
|
+
it('password-reset is rejected for SSO-only accounts (409)', async () => {
|
|
327
|
+
const { handler } = makeHandler([
|
|
328
|
+
admin(),
|
|
329
|
+
{ id: 'u2', email: 'sso@a.com', name: 'SSO', role: 'EDITOR', passwordHash: null },
|
|
330
|
+
]);
|
|
331
|
+
const res = await handler(req('/users/u2/password-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
332
|
+
expect(res.status).toBe(409);
|
|
333
|
+
});
|
|
334
|
+
it('password-reset sends a reset email for a normal user when email is configured', async () => {
|
|
335
|
+
const sent = [];
|
|
336
|
+
setEmailSender({ send: async (o) => void sent.push({ to: o.to }) });
|
|
337
|
+
const { handler } = makeHandler([
|
|
338
|
+
admin(),
|
|
339
|
+
{ id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR', passwordHash: 'h' },
|
|
340
|
+
]);
|
|
341
|
+
const res = await handler(req('/users/u2/password-reset', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
342
|
+
expect(res.status).toBe(200);
|
|
343
|
+
expect(sent).toEqual([{ to: 'p@a.com' }]);
|
|
344
|
+
});
|
|
345
|
+
it('transfer-content reassigns ownership to an active recipient', async () => {
|
|
346
|
+
const { handler } = makeHandler([
|
|
347
|
+
admin(),
|
|
348
|
+
{ id: 'u-owns', email: 'owns@a.com', name: 'Owns', role: 'EDITOR' },
|
|
349
|
+
{ id: 'u-recv', email: 'recv@a.com', name: 'Recv', role: 'EDITOR' },
|
|
350
|
+
]);
|
|
351
|
+
const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
|
|
352
|
+
toUserId: 'u-recv',
|
|
353
|
+
}));
|
|
354
|
+
expect(res.status).toBe(200);
|
|
355
|
+
const body = (await res.json());
|
|
356
|
+
expect(body.data.documents).toBe(2);
|
|
357
|
+
expect(body.data.total).toBe(2);
|
|
358
|
+
});
|
|
359
|
+
it('transfer-content rejects a missing recipient (400)', async () => {
|
|
360
|
+
const { handler } = makeHandler([
|
|
361
|
+
admin(),
|
|
362
|
+
{ id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
|
|
363
|
+
]);
|
|
364
|
+
const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {}));
|
|
365
|
+
expect(res.status).toBe(400);
|
|
366
|
+
});
|
|
367
|
+
it('transfer-content rejects transferring to a suspended user (409)', async () => {
|
|
368
|
+
const { handler } = makeHandler([
|
|
369
|
+
admin(),
|
|
370
|
+
{ id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
|
|
371
|
+
{ id: 'u-susp', email: 's@a.com', name: 'S', role: 'EDITOR', suspendedAt: new Date() },
|
|
372
|
+
]);
|
|
373
|
+
const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
|
|
374
|
+
toUserId: 'u-susp',
|
|
375
|
+
}));
|
|
376
|
+
expect(res.status).toBe(409);
|
|
377
|
+
});
|
|
378
|
+
it('transfer-content rejects transferring to the same user (400)', async () => {
|
|
379
|
+
const { handler } = makeHandler([
|
|
380
|
+
admin(),
|
|
381
|
+
{ id: 'u-owns', email: 'o@a.com', name: 'O', role: 'EDITOR' },
|
|
382
|
+
]);
|
|
383
|
+
const res = await handler(req('/users/u-owns/transfer-content', await headersFor('admin-1', 'ADMIN'), 'POST', {
|
|
384
|
+
toUserId: 'u-owns',
|
|
385
|
+
}));
|
|
386
|
+
expect(res.status).toBe(400);
|
|
387
|
+
});
|
|
388
|
+
it('lock then unlock toggles the locked state', async () => {
|
|
389
|
+
const { handler, db } = makeHandler([
|
|
390
|
+
admin(),
|
|
391
|
+
{ id: 'u2', email: 'p@a.com', name: 'P', role: 'EDITOR' },
|
|
392
|
+
]);
|
|
393
|
+
const lock = await handler(req('/users/u2/lock', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
394
|
+
expect(lock.status).toBe(200);
|
|
395
|
+
expect(db._users.find((u) => u.id === 'u2').lockedAt).toBeInstanceOf(Date);
|
|
396
|
+
const unlock = await handler(req('/users/u2/unlock', await headersFor('admin-1', 'ADMIN'), 'POST'));
|
|
397
|
+
expect(unlock.status).toBe(200);
|
|
398
|
+
expect(db._users.find((u) => u.id === 'u2').lockedAt).toBeNull();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
//# sourceMappingURL=users-detail.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users-detail.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/users-detail.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,28 +7,77 @@ import { setEmailSender } from '../../email/sender.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Integration tests for `POST /api/cms/users/invite`.
|
|
9
9
|
*
|
|
10
|
-
* The endpoint
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The endpoint records a first-class Invite (hashed single-use token) and
|
|
11
|
+
* provisions a placeholder user, then returns a set-password link (and/or
|
|
12
|
+
* emails it). It is admin-only, seat-aware, and Owner-gated for the Owner role.
|
|
13
13
|
*/
|
|
14
14
|
const SECRET = 'test-secret-for-users-invite-routes-1234567890abc';
|
|
15
15
|
function createMockDB(rows) {
|
|
16
|
-
const users =
|
|
16
|
+
const users = rows.map((r) => ({ isActive: true, ...r }));
|
|
17
17
|
const tokens = [];
|
|
18
|
+
const invites = [];
|
|
19
|
+
const invite = {
|
|
20
|
+
findMany: async ({ where } = {}) => {
|
|
21
|
+
let list = invites;
|
|
22
|
+
if (where?.status)
|
|
23
|
+
list = list.filter((i) => i.status === where.status);
|
|
24
|
+
return list;
|
|
25
|
+
},
|
|
26
|
+
findFirst: async ({ where }) => {
|
|
27
|
+
if (where?.email?.equals) {
|
|
28
|
+
const target = String(where.email.equals).toLowerCase();
|
|
29
|
+
return (invites.find((i) => i.email.toLowerCase() === target && (!where.status || i.status === where.status)) ?? null);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
findUnique: async ({ where }) => invites.find((i) => i.id === where.id) ?? null,
|
|
34
|
+
count: async ({ where } = {}) => {
|
|
35
|
+
let list = invites;
|
|
36
|
+
if (where?.status)
|
|
37
|
+
list = list.filter((i) => i.status === where.status);
|
|
38
|
+
return list.length;
|
|
39
|
+
},
|
|
40
|
+
create: async ({ data }) => {
|
|
41
|
+
const row = {
|
|
42
|
+
id: `inv_${invites.length + 1}`,
|
|
43
|
+
createdAt: new Date(),
|
|
44
|
+
...data,
|
|
45
|
+
};
|
|
46
|
+
invites.push(row);
|
|
47
|
+
return row;
|
|
48
|
+
},
|
|
49
|
+
update: async ({ where, data }) => {
|
|
50
|
+
const row = invites.find((i) => i.id === where.id);
|
|
51
|
+
Object.assign(row, data);
|
|
52
|
+
return row;
|
|
53
|
+
},
|
|
54
|
+
updateMany: async ({ where, data }) => {
|
|
55
|
+
let n = 0;
|
|
56
|
+
for (const i of invites) {
|
|
57
|
+
if (!where?.status || i.status === where.status) {
|
|
58
|
+
Object.assign(i, data);
|
|
59
|
+
n++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { count: n };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
18
65
|
return {
|
|
19
66
|
_users: users,
|
|
20
67
|
_tokens: tokens,
|
|
68
|
+
_invites: invites,
|
|
21
69
|
session: { findUnique: async () => ({ revokedAt: null }) },
|
|
22
70
|
user: {
|
|
23
|
-
// hasModel() probes for findMany; the invite route never calls it.
|
|
24
71
|
findMany: async () => users,
|
|
25
72
|
findUnique: async ({ where }) => users.find((u) => u.id === where.id) ?? null,
|
|
73
|
+
count: async ({ where } = {}) => users.filter((u) => (where?.role ? u.role === where.role : true) &&
|
|
74
|
+
(where?.isActive !== undefined ? (u.isActive ?? true) === where.isActive : true)).length,
|
|
26
75
|
findFirst: async ({ where }) => {
|
|
27
76
|
const target = String(where.email.equals).toLowerCase();
|
|
28
77
|
return users.find((u) => u.email.toLowerCase() === target) ?? null;
|
|
29
78
|
},
|
|
30
79
|
create: async ({ data }) => {
|
|
31
|
-
const u = { id: `u_${users.length + 1}`, ...data };
|
|
80
|
+
const u = { id: `u_${users.length + 1}`, isActive: true, ...data };
|
|
32
81
|
users.push(u);
|
|
33
82
|
return u;
|
|
34
83
|
},
|
|
@@ -40,6 +89,7 @@ function createMockDB(rows) {
|
|
|
40
89
|
return t;
|
|
41
90
|
},
|
|
42
91
|
},
|
|
92
|
+
invite,
|
|
43
93
|
};
|
|
44
94
|
}
|
|
45
95
|
async function roleHeaders(userId, role) {
|
|
@@ -91,37 +141,33 @@ describe('POST /api/cms/users/invite', () => {
|
|
|
91
141
|
const res = await handler(new Request('https://cms.example.com/api/cms/users/invite', {
|
|
92
142
|
method: 'POST',
|
|
93
143
|
headers: { 'content-type': 'application/json' },
|
|
94
|
-
body: JSON.stringify({ email: 'a@example.com', role: '
|
|
144
|
+
body: JSON.stringify({ email: 'a@example.com', role: 'editor' }),
|
|
95
145
|
}));
|
|
96
|
-
// CSRF runs before auth, so an anonymous POST without a token can return
|
|
97
|
-
// either — both prove the write was blocked.
|
|
98
146
|
expect([401, 403]).toContain(res.status);
|
|
99
147
|
});
|
|
100
148
|
it('rejects non-admin roles with 403', async () => {
|
|
101
149
|
const { handler, db } = makeHandler([
|
|
102
150
|
{ id: 'editor-1', email: 'ed@example.com', name: 'Ed', role: 'EDITOR' },
|
|
103
151
|
]);
|
|
104
|
-
const res = await handler(inviteRequest(await editorHeaders('editor-1'), { email: 'a@example.com', role: '
|
|
152
|
+
const res = await handler(inviteRequest(await editorHeaders('editor-1'), { email: 'a@example.com', role: 'editor' }));
|
|
105
153
|
expect(res.status).toBe(403);
|
|
106
|
-
expect(db.
|
|
154
|
+
expect(db._invites).toHaveLength(0);
|
|
107
155
|
});
|
|
108
|
-
it('
|
|
156
|
+
it('records an invite + token and returns a shareable link when email is unconfigured', async () => {
|
|
109
157
|
const { handler, db } = makeHandler([
|
|
110
158
|
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
111
159
|
]);
|
|
112
|
-
const res = await handler(inviteRequest(await adminHeaders('admin-1'), {
|
|
113
|
-
email: 'new@example.com',
|
|
114
|
-
name: 'New Person',
|
|
115
|
-
role: 'editor',
|
|
116
|
-
}));
|
|
160
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'new@example.com', role: 'editor' }));
|
|
117
161
|
expect(res.status).toBe(200);
|
|
118
162
|
const body = (await res.json());
|
|
119
163
|
expect(body.data.success).toBe(true);
|
|
120
164
|
expect(body.data.emailed).toBe(false);
|
|
121
|
-
expect(body.data.
|
|
165
|
+
expect(body.data.invite.role).toBe('EDITOR');
|
|
122
166
|
expect(body.data.inviteUrl).toContain('/admin/reset-password?token=');
|
|
123
167
|
expect(db._tokens).toHaveLength(1);
|
|
124
|
-
|
|
168
|
+
expect(db._invites).toHaveLength(1);
|
|
169
|
+
// The hashed token is stored — never the raw token.
|
|
170
|
+
expect(db._invites[0].tokenHash).not.toContain(body.data.inviteUrl.split('token=')[1]);
|
|
125
171
|
expect(db._users.map((u) => u.email)).toContain('new@example.com');
|
|
126
172
|
});
|
|
127
173
|
it('emails the invite (and hides the link) when a sender is registered', async () => {
|
|
@@ -134,33 +180,41 @@ describe('POST /api/cms/users/invite', () => {
|
|
|
134
180
|
const { handler } = makeHandler([
|
|
135
181
|
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
136
182
|
]);
|
|
137
|
-
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'new@example.com', role: '
|
|
183
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'new@example.com', role: 'viewer' }));
|
|
138
184
|
const body = (await res.json());
|
|
139
185
|
expect(body.data.emailed).toBe(true);
|
|
140
186
|
expect(body.data.inviteUrl).toBeUndefined();
|
|
141
187
|
expect(sent).toEqual([{ to: 'new@example.com' }]);
|
|
142
188
|
});
|
|
143
|
-
it('rejects a duplicate
|
|
189
|
+
it('rejects a duplicate existing user with 400', async () => {
|
|
144
190
|
const { handler, db } = makeHandler([
|
|
145
191
|
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
146
|
-
{ id: 'u-2', email: 'taken@example.com', name: 'Taken', role: '
|
|
192
|
+
{ id: 'u-2', email: 'taken@example.com', name: 'Taken', role: 'EDITOR' },
|
|
147
193
|
]);
|
|
148
|
-
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'Taken@Example.com', role: '
|
|
194
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'Taken@Example.com', role: 'editor' }));
|
|
149
195
|
expect(res.status).toBe(400);
|
|
150
|
-
expect(db.
|
|
196
|
+
expect(db._invites).toHaveLength(0);
|
|
151
197
|
});
|
|
152
198
|
it('rejects an invalid role with 400', async () => {
|
|
153
199
|
const { handler } = makeHandler([
|
|
154
200
|
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
155
201
|
]);
|
|
156
|
-
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'a@example.com', role: '
|
|
202
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'a@example.com', role: 'superuser' }));
|
|
157
203
|
expect(res.status).toBe(400);
|
|
158
204
|
});
|
|
205
|
+
it('forbids a non-Owner from inviting an Owner (403)', async () => {
|
|
206
|
+
const { handler, db } = makeHandler([
|
|
207
|
+
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
208
|
+
]);
|
|
209
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'owner@example.com', role: 'owner' }));
|
|
210
|
+
expect(res.status).toBe(403);
|
|
211
|
+
expect(db._invites).toHaveLength(0);
|
|
212
|
+
});
|
|
159
213
|
it('requires an email', async () => {
|
|
160
214
|
const { handler } = makeHandler([
|
|
161
215
|
{ id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
|
|
162
216
|
]);
|
|
163
|
-
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { role: '
|
|
217
|
+
const res = await handler(inviteRequest(await adminHeaders('admin-1'), { role: 'editor' }));
|
|
164
218
|
expect(res.status).toBe(400);
|
|
165
219
|
});
|
|
166
220
|
});
|