@actuate-media/cms-core 0.37.0 → 0.39.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.
Files changed (72) hide show
  1. package/dist/__tests__/api/ai-status.test.d.ts +2 -0
  2. package/dist/__tests__/api/ai-status.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/ai-status.test.js +110 -0
  4. package/dist/__tests__/api/ai-status.test.js.map +1 -0
  5. package/dist/__tests__/api/totp-login.test.d.ts +2 -0
  6. package/dist/__tests__/api/totp-login.test.d.ts.map +1 -0
  7. package/dist/__tests__/api/totp-login.test.js +115 -0
  8. package/dist/__tests__/api/totp-login.test.js.map +1 -0
  9. package/dist/__tests__/api/users-invite.test.d.ts +2 -0
  10. package/dist/__tests__/api/users-invite.test.d.ts.map +1 -0
  11. package/dist/__tests__/api/users-invite.test.js +167 -0
  12. package/dist/__tests__/api/users-invite.test.js.map +1 -0
  13. package/dist/__tests__/auth/invite.test.d.ts +2 -0
  14. package/dist/__tests__/auth/invite.test.d.ts.map +1 -0
  15. package/dist/__tests__/auth/invite.test.js +151 -0
  16. package/dist/__tests__/auth/invite.test.js.map +1 -0
  17. package/dist/__tests__/auth/reset.test.js +52 -1
  18. package/dist/__tests__/auth/reset.test.js.map +1 -1
  19. package/dist/__tests__/auth/totp.test.d.ts +2 -0
  20. package/dist/__tests__/auth/totp.test.d.ts.map +1 -0
  21. package/dist/__tests__/auth/totp.test.js +52 -0
  22. package/dist/__tests__/auth/totp.test.js.map +1 -0
  23. package/dist/__tests__/email/sender.test.d.ts +2 -0
  24. package/dist/__tests__/email/sender.test.d.ts.map +1 -0
  25. package/dist/__tests__/email/sender.test.js +33 -0
  26. package/dist/__tests__/email/sender.test.js.map +1 -0
  27. package/dist/__tests__/middleware.test.js +30 -0
  28. package/dist/__tests__/middleware.test.js.map +1 -1
  29. package/dist/__tests__/setup/index.test.js +37 -2
  30. package/dist/__tests__/setup/index.test.js.map +1 -1
  31. package/dist/api/handlers.d.ts.map +1 -1
  32. package/dist/api/handlers.js +141 -5
  33. package/dist/api/handlers.js.map +1 -1
  34. package/dist/auth/index.d.ts +2 -0
  35. package/dist/auth/index.d.ts.map +1 -1
  36. package/dist/auth/index.js +1 -0
  37. package/dist/auth/index.js.map +1 -1
  38. package/dist/auth/invite-email.d.ts +12 -0
  39. package/dist/auth/invite-email.d.ts.map +1 -0
  40. package/dist/auth/invite-email.js +57 -0
  41. package/dist/auth/invite-email.js.map +1 -0
  42. package/dist/auth/invite.d.ts +37 -0
  43. package/dist/auth/invite.d.ts.map +1 -0
  44. package/dist/auth/invite.js +104 -0
  45. package/dist/auth/invite.js.map +1 -0
  46. package/dist/auth/reset.d.ts +6 -0
  47. package/dist/auth/reset.d.ts.map +1 -1
  48. package/dist/auth/reset.js +11 -3
  49. package/dist/auth/reset.js.map +1 -1
  50. package/dist/auth/totp.d.ts +12 -0
  51. package/dist/auth/totp.d.ts.map +1 -1
  52. package/dist/auth/totp.js +24 -0
  53. package/dist/auth/totp.js.map +1 -1
  54. package/dist/email/sender.d.ts +35 -0
  55. package/dist/email/sender.d.ts.map +1 -0
  56. package/dist/email/sender.js +25 -0
  57. package/dist/email/sender.js.map +1 -0
  58. package/dist/forms/notify.d.ts.map +1 -1
  59. package/dist/forms/notify.js +3 -2
  60. package/dist/forms/notify.js.map +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/middleware.d.ts.map +1 -1
  66. package/dist/middleware.js +60 -1
  67. package/dist/middleware.js.map +1 -1
  68. package/dist/setup/index.d.ts +14 -0
  69. package/dist/setup/index.d.ts.map +1 -1
  70. package/dist/setup/index.js +29 -3
  71. package/dist/setup/index.js.map +1 -1
  72. package/package.json +6 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai-status.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-status.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/ai-status.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { initDB } from '../../db.js';
4
+ import { generateApiKey } from '../../security/api-key-enhanced.js';
5
+ /**
6
+ * Contract tests for GET /ai/status.
7
+ *
8
+ * The AI tab in the admin is read-only: AI is configured server-side via the
9
+ * plugin + environment variables, never from the dashboard. This endpoint
10
+ * powers an honest status panel, so it must:
11
+ * - require an authenticated admin,
12
+ * - return a `{ installed, configured, provider }` shape,
13
+ * - only hint at which provider env var is present (never the key value).
14
+ */
15
+ const VALID_SECRET = 'a'.repeat(48);
16
+ function createMockDB(apiKey) {
17
+ return {
18
+ apiKey: {
19
+ findMany: async () => (apiKey ? [apiKey] : []),
20
+ findUnique: async ({ where }) => apiKey && apiKey.keyHash === where.keyHash ? apiKey : null,
21
+ update: async () => apiKey,
22
+ },
23
+ document: {
24
+ findFirst: async () => null,
25
+ findMany: async () => [],
26
+ count: async () => 0,
27
+ },
28
+ user: {
29
+ findMany: async () => [],
30
+ findUnique: async () => null,
31
+ count: async () => 1,
32
+ },
33
+ auditLog: { create: async () => ({ id: 'log1' }), count: async () => 0 },
34
+ };
35
+ }
36
+ async function makeKey(scopes) {
37
+ const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
38
+ return {
39
+ key,
40
+ record: {
41
+ id: 'apikey-1',
42
+ keyHash,
43
+ keyPrefix,
44
+ userId: 'user-1',
45
+ scopes,
46
+ ipRestrictions: null,
47
+ expiresAt: null,
48
+ lastUsedAt: null,
49
+ revokedAt: null,
50
+ },
51
+ };
52
+ }
53
+ beforeEach(() => {
54
+ delete globalThis.__actuateConfig;
55
+ process.env.CMS_SECRET = VALID_SECRET;
56
+ });
57
+ afterEach(() => {
58
+ delete process.env.ANTHROPIC_API_KEY;
59
+ delete process.env.OPENAI_API_KEY;
60
+ });
61
+ describe('GET /ai/status', () => {
62
+ it('requires authentication', async () => {
63
+ const db = createMockDB();
64
+ initDB(db);
65
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
66
+ const res = await handler(new Request('https://example.com/api/cms/ai/status'));
67
+ expect(res.status).toBe(401);
68
+ });
69
+ it('rejects non-admin API keys', async () => {
70
+ const { key, record } = await makeKey({ media: true });
71
+ const db = createMockDB(record);
72
+ initDB(db);
73
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
74
+ const res = await handler(new Request('https://example.com/api/cms/ai/status', {
75
+ headers: { Authorization: `Bearer ${key}` },
76
+ }));
77
+ expect(res.status).toBe(403);
78
+ });
79
+ it('returns the status shape for an admin and never configures from the dashboard', async () => {
80
+ const { key, record } = await makeKey({ admin: true });
81
+ const db = createMockDB(record);
82
+ initDB(db);
83
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
84
+ const res = await handler(new Request('https://example.com/api/cms/ai/status', {
85
+ headers: { Authorization: `Bearer ${key}` },
86
+ }));
87
+ expect(res.status).toBe(200);
88
+ const body = (await res.json());
89
+ expect(typeof body.data.installed).toBe('boolean');
90
+ // No provider is registered in the test process, so it must report unconfigured.
91
+ expect(body.data.configured).toBe(false);
92
+ expect(body.data.provider).toBeNull();
93
+ });
94
+ it('hints at the configured provider env var without exposing the secret', async () => {
95
+ process.env.ANTHROPIC_API_KEY = 'sk-ant-super-secret-value';
96
+ const { key, record } = await makeKey({ admin: true });
97
+ const db = createMockDB(record);
98
+ initDB(db);
99
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
100
+ const res = await handler(new Request('https://example.com/api/cms/ai/status', {
101
+ headers: { Authorization: `Bearer ${key}` },
102
+ }));
103
+ expect(res.status).toBe(200);
104
+ const body = (await res.json());
105
+ expect(body.data.provider).toBe('anthropic');
106
+ // The hint must never include the actual key value.
107
+ expect(JSON.stringify(body)).not.toContain('sk-ant-super-secret-value');
108
+ });
109
+ });
110
+ //# sourceMappingURL=ai-status.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-status.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/ai-status.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=totp-login.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp-login.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/totp-login.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { initDB } from '../../db.js';
4
+ import { encryptSecret, encryptStringArray } from '../../security/secret-storage.js';
5
+ import { createMfaPendingToken, computeRequestFingerprint } from '../../auth/mfa-pending.js';
6
+ import { generateTOTPSecret } from '../../auth/totp.js';
7
+ /**
8
+ * Contract test for the backup-code recovery path of POST /auth/totp/login.
9
+ *
10
+ * Backup codes are generated and shown at enrollment but, before this, were
11
+ * never consumed anywhere — a lost authenticator meant permanent lockout. This
12
+ * proves a valid code signs the user in, is single-use (consumed on success),
13
+ * and that an unknown code is rejected.
14
+ */
15
+ const SECRET = 'a'.repeat(48);
16
+ const ENCRYPTION_KEY = 'b'.repeat(64); // 32 bytes hex
17
+ const BACKUP_CODES = ['aaaa1111', 'bbbb2222', 'cccc3333'];
18
+ async function createMockDB(state) {
19
+ const totpSecret = await encryptSecret(generateTOTPSecret());
20
+ return {
21
+ user: {
22
+ findUnique: async () => ({
23
+ id: 'user-1',
24
+ email: 'admin@example.com',
25
+ name: 'Admin',
26
+ role: 'ADMIN',
27
+ totpSecret,
28
+ totpEnabled: true,
29
+ backupCodes: await encryptStringArray(state.backupCodes),
30
+ isActive: true,
31
+ }),
32
+ update: async ({ data }) => {
33
+ // The handler re-encrypts the remaining codes; decrypt-by-count is
34
+ // enough for the assertion (we only check how many remain).
35
+ state.backupCodes = state.backupCodes.slice(0, data.backupCodes.length);
36
+ return { id: 'user-1' };
37
+ },
38
+ },
39
+ document: {
40
+ findFirst: async () => ({ data: {} }),
41
+ },
42
+ session: {
43
+ findMany: async () => [],
44
+ create: async () => ({ id: 'sess-1' }),
45
+ updateMany: async () => ({ count: 0 }),
46
+ },
47
+ auditLog: {
48
+ create: async () => ({ id: 'log-1' }),
49
+ },
50
+ };
51
+ }
52
+ async function makePendingToken(ip, userAgent) {
53
+ const fingerprint = await computeRequestFingerprint(ip, userAgent);
54
+ return createMfaPendingToken({ userId: 'user-1', fingerprint }, SECRET);
55
+ }
56
+ beforeEach(() => {
57
+ delete globalThis.__actuateConfig;
58
+ process.env.CMS_SECRET = SECRET;
59
+ process.env.CMS_ENCRYPTION_KEY = ENCRYPTION_KEY;
60
+ });
61
+ describe('POST /auth/totp/login — backup code recovery', () => {
62
+ it('signs in with a valid backup code and consumes it (single-use)', async () => {
63
+ const ip = '203.0.113.10';
64
+ const ua = 'vitest-agent';
65
+ const state = { backupCodes: [...BACKUP_CODES] };
66
+ const db = await createMockDB(state);
67
+ initDB(db);
68
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
69
+ const token = await makePendingToken(ip, ua);
70
+ const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
71
+ method: 'POST',
72
+ headers: { 'x-real-ip': ip, 'user-agent': ua, 'content-type': 'application/json' },
73
+ body: JSON.stringify({ mfaPendingToken: token, code: 'BBBB-2222' }),
74
+ }));
75
+ expect(res.status).toBe(200);
76
+ expect(res.headers.get('set-cookie')).toContain('actuate_session=');
77
+ // The used code was removed — one fewer remains.
78
+ expect(state.backupCodes).toHaveLength(BACKUP_CODES.length - 1);
79
+ });
80
+ it('rejects an unknown code', async () => {
81
+ const ip = '203.0.113.11';
82
+ const ua = 'vitest-agent';
83
+ const state = { backupCodes: [...BACKUP_CODES] };
84
+ const db = await createMockDB(state);
85
+ initDB(db);
86
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
87
+ const token = await makePendingToken(ip, ua);
88
+ const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
89
+ method: 'POST',
90
+ headers: { 'x-real-ip': ip, 'user-agent': ua, 'content-type': 'application/json' },
91
+ body: JSON.stringify({ mfaPendingToken: token, code: 'deadbeef' }),
92
+ }));
93
+ expect(res.status).toBe(401);
94
+ expect(state.backupCodes).toHaveLength(BACKUP_CODES.length);
95
+ });
96
+ it('rejects a stale pending token from a different device fingerprint', async () => {
97
+ const state = { backupCodes: [...BACKUP_CODES] };
98
+ const db = await createMockDB(state);
99
+ initDB(db);
100
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
101
+ // Token minted for one device, redeemed from another (different UA).
102
+ const token = await makePendingToken('203.0.113.12', 'device-a');
103
+ const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
104
+ method: 'POST',
105
+ headers: {
106
+ 'x-real-ip': '203.0.113.12',
107
+ 'user-agent': 'device-b',
108
+ 'content-type': 'application/json',
109
+ },
110
+ body: JSON.stringify({ mfaPendingToken: token, code: 'aaaa1111' }),
111
+ }));
112
+ expect(res.status).toBe(401);
113
+ });
114
+ });
115
+ //# sourceMappingURL=totp-login.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp-login.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/totp-login.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=users-invite.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users-invite.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/users-invite.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
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
+ /**
8
+ * Integration tests for `POST /api/cms/users/invite`.
9
+ *
10
+ * The endpoint provisions a user with an unusable password and a single-use
11
+ * reset token, then returns a set-password link (and/or emails it). It is
12
+ * admin-only — the only multi-user provisioning path in the product.
13
+ */
14
+ const SECRET = 'test-secret-for-users-invite-routes-1234567890abc';
15
+ function createMockDB(rows) {
16
+ const users = [...rows];
17
+ const tokens = [];
18
+ return {
19
+ _users: users,
20
+ _tokens: tokens,
21
+ session: { findUnique: async () => ({ revokedAt: null }) },
22
+ user: {
23
+ // hasModel() probes for findMany; the invite route never calls it.
24
+ findMany: async () => users,
25
+ findUnique: async ({ where }) => users.find((u) => u.id === where.id) ?? null,
26
+ findFirst: async ({ where }) => {
27
+ const target = String(where.email.equals).toLowerCase();
28
+ return users.find((u) => u.email.toLowerCase() === target) ?? null;
29
+ },
30
+ create: async ({ data }) => {
31
+ const u = { id: `u_${users.length + 1}`, ...data };
32
+ users.push(u);
33
+ return u;
34
+ },
35
+ },
36
+ passwordResetToken: {
37
+ create: async ({ data }) => {
38
+ const t = { id: `rt_${tokens.length + 1}`, ...data };
39
+ tokens.push(t);
40
+ return t;
41
+ },
42
+ },
43
+ };
44
+ }
45
+ async function roleHeaders(userId, role) {
46
+ const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
47
+ const csrf = await generateCsrfToken();
48
+ return {
49
+ cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
50
+ 'x-csrf-token': csrf,
51
+ 'content-type': 'application/json',
52
+ };
53
+ }
54
+ const adminHeaders = (userId) => roleHeaders(userId, 'ADMIN');
55
+ const editorHeaders = (userId) => roleHeaders(userId, 'EDITOR');
56
+ describe('POST /api/cms/users/invite', () => {
57
+ const original = {
58
+ CMS_SECRET: process.env.CMS_SECRET,
59
+ CMS_SESSION_SECRET: process.env.CMS_SESSION_SECRET,
60
+ NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
61
+ };
62
+ beforeEach(() => {
63
+ process.env.CMS_SECRET = SECRET;
64
+ delete process.env.CMS_SESSION_SECRET;
65
+ process.env.NEXT_PUBLIC_SITE_URL = 'https://cms.example.com';
66
+ setEmailSender(null);
67
+ });
68
+ afterEach(() => {
69
+ setEmailSender(null);
70
+ for (const [k, v] of Object.entries(original)) {
71
+ if (v === undefined)
72
+ delete process.env[k];
73
+ else
74
+ process.env[k] = v;
75
+ }
76
+ });
77
+ function makeHandler(rows = []) {
78
+ const db = createMockDB(rows);
79
+ initDB(db);
80
+ return { handler: handleActuateAPI({ prismaClient: db }), db };
81
+ }
82
+ function inviteRequest(headers, body) {
83
+ return new Request('https://cms.example.com/api/cms/users/invite', {
84
+ method: 'POST',
85
+ headers,
86
+ body: JSON.stringify(body),
87
+ });
88
+ }
89
+ it('rejects anonymous writes (401 from auth or 403 from CSRF)', async () => {
90
+ const { handler } = makeHandler();
91
+ const res = await handler(new Request('https://cms.example.com/api/cms/users/invite', {
92
+ method: 'POST',
93
+ headers: { 'content-type': 'application/json' },
94
+ body: JSON.stringify({ email: 'a@example.com', role: 'author' }),
95
+ }));
96
+ // CSRF runs before auth, so an anonymous POST without a token can return
97
+ // either — both prove the write was blocked.
98
+ expect([401, 403]).toContain(res.status);
99
+ });
100
+ it('rejects non-admin roles with 403', async () => {
101
+ const { handler, db } = makeHandler([
102
+ { id: 'editor-1', email: 'ed@example.com', name: 'Ed', role: 'EDITOR' },
103
+ ]);
104
+ const res = await handler(inviteRequest(await editorHeaders('editor-1'), { email: 'a@example.com', role: 'author' }));
105
+ expect(res.status).toBe(403);
106
+ expect(db._users).toHaveLength(1);
107
+ });
108
+ it('creates the user + token and returns a shareable link when email is unconfigured', async () => {
109
+ const { handler, db } = makeHandler([
110
+ { id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
111
+ ]);
112
+ const res = await handler(inviteRequest(await adminHeaders('admin-1'), {
113
+ email: 'new@example.com',
114
+ name: 'New Person',
115
+ role: 'editor',
116
+ }));
117
+ expect(res.status).toBe(200);
118
+ const body = (await res.json());
119
+ expect(body.data.success).toBe(true);
120
+ expect(body.data.emailed).toBe(false);
121
+ expect(body.data.user.role).toBe('EDITOR');
122
+ expect(body.data.inviteUrl).toContain('/admin/reset-password?token=');
123
+ expect(db._tokens).toHaveLength(1);
124
+ // The created user is the second row (after the admin fixture).
125
+ expect(db._users.map((u) => u.email)).toContain('new@example.com');
126
+ });
127
+ it('emails the invite (and hides the link) when a sender is registered', async () => {
128
+ const sent = [];
129
+ setEmailSender({
130
+ send: async (o) => {
131
+ sent.push({ to: o.to });
132
+ },
133
+ });
134
+ const { handler } = makeHandler([
135
+ { id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
136
+ ]);
137
+ const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'new@example.com', role: 'author' }));
138
+ const body = (await res.json());
139
+ expect(body.data.emailed).toBe(true);
140
+ expect(body.data.inviteUrl).toBeUndefined();
141
+ expect(sent).toEqual([{ to: 'new@example.com' }]);
142
+ });
143
+ it('rejects a duplicate email with 400', async () => {
144
+ const { handler, db } = makeHandler([
145
+ { id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
146
+ { id: 'u-2', email: 'taken@example.com', name: 'Taken', role: 'AUTHOR' },
147
+ ]);
148
+ const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'Taken@Example.com', role: 'author' }));
149
+ expect(res.status).toBe(400);
150
+ expect(db._users).toHaveLength(2);
151
+ });
152
+ it('rejects an invalid role with 400', async () => {
153
+ const { handler } = makeHandler([
154
+ { id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
155
+ ]);
156
+ const res = await handler(inviteRequest(await adminHeaders('admin-1'), { email: 'a@example.com', role: 'viewer' }));
157
+ expect(res.status).toBe(400);
158
+ });
159
+ it('requires an email', async () => {
160
+ const { handler } = makeHandler([
161
+ { id: 'admin-1', email: 'admin@example.com', name: 'Admin', role: 'ADMIN' },
162
+ ]);
163
+ const res = await handler(inviteRequest(await adminHeaders('admin-1'), { role: 'author' }));
164
+ expect(res.status).toBe(400);
165
+ });
166
+ });
167
+ //# sourceMappingURL=users-invite.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users-invite.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/users-invite.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=invite.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/auth/invite.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { createUserInvite, normalizeInviteRole } from '../../auth/invite.js';
3
+ import { hashToken } from '../../auth/reset.js';
4
+ import { setEmailSender } from '../../email/sender.js';
5
+ function createFakeDb(initial = {}) {
6
+ const users = initial.users ?? [];
7
+ const tokens = [];
8
+ return {
9
+ users,
10
+ tokens,
11
+ user: {
12
+ findFirst: vi.fn(async ({ where }) => {
13
+ const target = where.email.equals.toLowerCase();
14
+ return users.find((u) => u.email.toLowerCase() === target) ?? null;
15
+ }),
16
+ create: vi.fn(async ({ data }) => {
17
+ const u = { id: `u_${users.length + 1}`, ...data };
18
+ users.push(u);
19
+ return u;
20
+ }),
21
+ },
22
+ passwordResetToken: {
23
+ create: vi.fn(async ({ data }) => {
24
+ const t = { id: `rt_${tokens.length + 1}`, ...data };
25
+ tokens.push(t);
26
+ return t;
27
+ }),
28
+ },
29
+ };
30
+ }
31
+ const BASE_CONFIG = { siteUrl: 'https://example.com', adminPath: '/admin' };
32
+ afterEach(() => {
33
+ setEmailSender(null);
34
+ });
35
+ describe('normalizeInviteRole', () => {
36
+ it('upper-cases and validates known roles', () => {
37
+ expect(normalizeInviteRole('editor')).toBe('EDITOR');
38
+ expect(normalizeInviteRole(' Admin ')).toBe('ADMIN');
39
+ expect(normalizeInviteRole('AUTHOR')).toBe('AUTHOR');
40
+ expect(normalizeInviteRole('client')).toBe('CLIENT');
41
+ });
42
+ it('rejects unknown / empty roles', () => {
43
+ expect(normalizeInviteRole('viewer')).toBeNull();
44
+ expect(normalizeInviteRole('')).toBeNull();
45
+ expect(normalizeInviteRole(undefined)).toBeNull();
46
+ });
47
+ });
48
+ describe('createUserInvite', () => {
49
+ it('creates an active user with a valid role and an unusable random password', async () => {
50
+ const db = createFakeDb();
51
+ const result = await createUserInvite(db, { email: 'New@Example.com ', name: ' Jane ', role: 'editor' }, BASE_CONFIG);
52
+ expect(result.success).toBe(true);
53
+ expect(db.user.create).toHaveBeenCalledOnce();
54
+ const created = db.users[0];
55
+ expect(created.email).toBe('new@example.com');
56
+ expect(created.name).toBe('Jane');
57
+ expect(created.role).toBe('EDITOR');
58
+ expect(created.isActive).toBe(true);
59
+ expect(created.isApproved).toBe(true);
60
+ expect(created.emailVerified).toBe(false);
61
+ // Password is a real hash (not blank/plaintext) so the account can't be
62
+ // logged into until the invitee sets their own password.
63
+ expect(created.passwordHash).toMatch(/^pbkdf2:/);
64
+ });
65
+ it('defaults the name to the email local-part when omitted', async () => {
66
+ const db = createFakeDb();
67
+ await createUserInvite(db, { email: 'sam@example.com', role: 'author' }, BASE_CONFIG);
68
+ expect(db.users[0].name).toBe('sam');
69
+ });
70
+ it('creates a single-use reset token and returns a shareable link when no email sender is set', async () => {
71
+ const db = createFakeDb();
72
+ const result = await createUserInvite(db, { email: 'a@example.com', role: 'author' }, BASE_CONFIG);
73
+ expect(result.emailed).toBe(false);
74
+ expect(result.inviteUrl).toContain('https://example.com/admin/reset-password?token=');
75
+ expect(db.passwordResetToken.create).toHaveBeenCalledOnce();
76
+ // The token in the link is the raw token; the DB stores only its hash.
77
+ const tokenInUrl = result.inviteUrl.match(/token=([0-9a-f]{64})/)?.[1];
78
+ expect(tokenInUrl).toBeDefined();
79
+ expect(hashToken(tokenInUrl)).toBe(db.tokens[0].tokenHash);
80
+ // ~7 day expiry
81
+ const ttl = db.tokens[0].expiresAt.getTime() - Date.now();
82
+ expect(ttl).toBeGreaterThan(6.9 * 24 * 60 * 60 * 1000);
83
+ });
84
+ it('emails the invite via the registered sender and hides the link from the result', async () => {
85
+ const send = vi.fn(async (_o) => { });
86
+ setEmailSender({ send });
87
+ const db = createFakeDb();
88
+ const result = await createUserInvite(db, { email: 'a@example.com', name: 'A', role: 'admin' }, { ...BASE_CONFIG, invitedByName: 'Boss', siteName: 'Acme' });
89
+ expect(result.success).toBe(true);
90
+ expect(result.emailed).toBe(true);
91
+ expect(result.inviteUrl).toBeUndefined();
92
+ expect(send).toHaveBeenCalledOnce();
93
+ const sent = send.mock.calls[0][0];
94
+ expect(sent.to).toBe('a@example.com');
95
+ expect(sent.html).toContain('/admin/reset-password?token=');
96
+ });
97
+ it('builds the link from the configured admin path', async () => {
98
+ const db = createFakeDb();
99
+ const result = await createUserInvite(db, { email: 'a@example.com', role: 'author' }, {
100
+ siteUrl: 'https://example.com/',
101
+ adminPath: '/securelogin',
102
+ });
103
+ expect(result.inviteUrl).toContain('https://example.com/securelogin/reset-password?token=');
104
+ expect(result.inviteUrl).not.toContain('//securelogin');
105
+ });
106
+ it('rejects a duplicate email (case-insensitive) without creating a user', async () => {
107
+ const db = createFakeDb({
108
+ users: [
109
+ {
110
+ id: 'u_1',
111
+ email: 'taken@example.com',
112
+ name: 'Taken',
113
+ role: 'EDITOR',
114
+ passwordHash: 'pbkdf2:x',
115
+ isActive: true,
116
+ isApproved: true,
117
+ emailVerified: true,
118
+ },
119
+ ],
120
+ });
121
+ const result = await createUserInvite(db, { email: 'Taken@Example.com', role: 'author' }, BASE_CONFIG);
122
+ expect(result.success).toBe(false);
123
+ expect(result.error).toMatch(/already exists/i);
124
+ expect(db.user.create).not.toHaveBeenCalled();
125
+ });
126
+ it('rejects an invalid email', async () => {
127
+ const db = createFakeDb();
128
+ const result = await createUserInvite(db, { email: 'not-an-email', role: 'author' }, BASE_CONFIG);
129
+ expect(result.success).toBe(false);
130
+ expect(db.user.create).not.toHaveBeenCalled();
131
+ });
132
+ it('rejects an invalid role', async () => {
133
+ const db = createFakeDb();
134
+ const result = await createUserInvite(db, { email: 'a@example.com', role: 'viewer' }, BASE_CONFIG);
135
+ expect(result.success).toBe(false);
136
+ expect(result.error).toMatch(/role/i);
137
+ expect(db.user.create).not.toHaveBeenCalled();
138
+ });
139
+ it('still succeeds when the email send throws (link returned for manual sharing)', async () => {
140
+ const send = vi.fn(async () => {
141
+ throw new Error('smtp down');
142
+ });
143
+ setEmailSender({ send });
144
+ const db = createFakeDb();
145
+ const result = await createUserInvite(db, { email: 'a@example.com', role: 'author' }, BASE_CONFIG);
146
+ expect(result.success).toBe(true);
147
+ expect(result.emailed).toBe(false);
148
+ expect(result.inviteUrl).toContain('/admin/reset-password?token=');
149
+ });
150
+ });
151
+ //# sourceMappingURL=invite.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.test.js","sourceRoot":"","sources":["../../../src/__tests__/auth/invite.test.ts"],"names":[],"mappings":"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"}