@varshylinc/team-management 0.1.0 → 0.2.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 (254) hide show
  1. package/dist/client/actions.d.ts +20 -0
  2. package/dist/client/actions.d.ts.map +1 -0
  3. package/dist/client/actions.js +23 -0
  4. package/dist/client/actions.js.map +1 -0
  5. package/dist/client/api.d.ts +74 -0
  6. package/dist/client/api.d.ts.map +1 -0
  7. package/dist/client/api.js +245 -0
  8. package/dist/client/api.js.map +1 -0
  9. package/dist/client/components/AddMemberForm.d.ts +12 -0
  10. package/dist/client/components/AddMemberForm.d.ts.map +1 -0
  11. package/dist/client/components/AddMemberForm.js +37 -0
  12. package/dist/client/components/AddMemberForm.js.map +1 -0
  13. package/dist/client/components/AuditEventRow.d.ts +7 -0
  14. package/dist/client/components/AuditEventRow.d.ts.map +1 -0
  15. package/dist/client/components/AuditEventRow.js +20 -0
  16. package/dist/client/components/AuditEventRow.js.map +1 -0
  17. package/dist/client/components/CascadePreview.d.ts +11 -0
  18. package/dist/client/components/CascadePreview.d.ts.map +1 -0
  19. package/dist/client/components/CascadePreview.js +7 -0
  20. package/dist/client/components/CascadePreview.js.map +1 -0
  21. package/dist/client/components/DangerZoneCard.d.ts +10 -0
  22. package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
  23. package/dist/client/components/DangerZoneCard.js +28 -0
  24. package/dist/client/components/DangerZoneCard.js.map +1 -0
  25. package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
  26. package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
  27. package/dist/client/components/InvitationCodeDisplay.js +26 -0
  28. package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
  29. package/dist/client/components/InviteForm.d.ts +7 -0
  30. package/dist/client/components/InviteForm.d.ts.map +1 -0
  31. package/dist/client/components/InviteForm.js +35 -0
  32. package/dist/client/components/InviteForm.js.map +1 -0
  33. package/dist/client/components/MemberRow.d.ts +10 -0
  34. package/dist/client/components/MemberRow.d.ts.map +1 -0
  35. package/dist/client/components/MemberRow.js +17 -0
  36. package/dist/client/components/MemberRow.js.map +1 -0
  37. package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
  38. package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
  39. package/dist/client/components/OrgPeopleRoster.js +13 -0
  40. package/dist/client/components/OrgPeopleRoster.js.map +1 -0
  41. package/dist/client/components/PendingTransferBanner.d.ts +10 -0
  42. package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
  43. package/dist/client/components/PendingTransferBanner.js +48 -0
  44. package/dist/client/components/PendingTransferBanner.js.map +1 -0
  45. package/dist/client/components/PlaceholderCard.d.ts +7 -0
  46. package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
  47. package/dist/client/components/PlaceholderCard.js +15 -0
  48. package/dist/client/components/PlaceholderCard.js.map +1 -0
  49. package/dist/client/components/RoleBadge.d.ts +5 -0
  50. package/dist/client/components/RoleBadge.d.ts.map +1 -0
  51. package/dist/client/components/RoleBadge.js +17 -0
  52. package/dist/client/components/RoleBadge.js.map +1 -0
  53. package/dist/client/components/RoleSelect.d.ts +10 -0
  54. package/dist/client/components/RoleSelect.d.ts.map +1 -0
  55. package/dist/client/components/RoleSelect.js +12 -0
  56. package/dist/client/components/RoleSelect.js.map +1 -0
  57. package/dist/client/components/SeatUsagePanel.d.ts +6 -0
  58. package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
  59. package/dist/client/components/SeatUsagePanel.js +13 -0
  60. package/dist/client/components/SeatUsagePanel.js.map +1 -0
  61. package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
  62. package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
  63. package/dist/client/hooks/useCurrentMembership.js +20 -0
  64. package/dist/client/hooks/useCurrentMembership.js.map +1 -0
  65. package/dist/client/hooks/useMembers.d.ts +10 -0
  66. package/dist/client/hooks/useMembers.d.ts.map +1 -0
  67. package/dist/client/hooks/useMembers.js +20 -0
  68. package/dist/client/hooks/useMembers.js.map +1 -0
  69. package/dist/client/hooks/useOrgMembers.d.ts +20 -0
  70. package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
  71. package/dist/client/hooks/useOrgMembers.js +63 -0
  72. package/dist/client/hooks/useOrgMembers.js.map +1 -0
  73. package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
  74. package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
  75. package/dist/client/hooks/usePendingInvitations.js +20 -0
  76. package/dist/client/hooks/usePendingInvitations.js.map +1 -0
  77. package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
  78. package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
  79. package/dist/client/hooks/usePendingTransfer.js +23 -0
  80. package/dist/client/hooks/usePendingTransfer.js.map +1 -0
  81. package/dist/client/index.d.ts +31 -0
  82. package/dist/client/index.d.ts.map +1 -0
  83. package/{src/client/index.ts → dist/client/index.js} +6 -54
  84. package/dist/client/index.js.map +1 -0
  85. package/dist/client/pages/AuditLogPage.d.ts +6 -0
  86. package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
  87. package/dist/client/pages/AuditLogPage.js +51 -0
  88. package/dist/client/pages/AuditLogPage.js.map +1 -0
  89. package/dist/client/pages/EmailChangePage.d.ts +8 -0
  90. package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
  91. package/dist/client/pages/EmailChangePage.js +52 -0
  92. package/dist/client/pages/EmailChangePage.js.map +1 -0
  93. package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
  94. package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
  95. package/dist/client/pages/InvitationAcceptPage.js +42 -0
  96. package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
  97. package/dist/client/pages/InvitationCodePage.d.ts +6 -0
  98. package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
  99. package/dist/client/pages/InvitationCodePage.js +28 -0
  100. package/dist/client/pages/InvitationCodePage.js.map +1 -0
  101. package/dist/client/pages/MembersPage.d.ts +6 -0
  102. package/dist/client/pages/MembersPage.d.ts.map +1 -0
  103. package/dist/client/pages/MembersPage.js +67 -0
  104. package/dist/client/pages/MembersPage.js.map +1 -0
  105. package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
  106. package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
  107. package/dist/client/pages/OrgPeoplePage.js +40 -0
  108. package/dist/client/pages/OrgPeoplePage.js.map +1 -0
  109. package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
  110. package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
  111. package/dist/client/pages/OrgSettingsPage.js +78 -0
  112. package/dist/client/pages/OrgSettingsPage.js.map +1 -0
  113. package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
  114. package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
  115. package/dist/client/pages/OwnershipTransferPage.js +68 -0
  116. package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
  117. package/dist/client/pages/PasswordResetPage.d.ts +6 -0
  118. package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
  119. package/dist/client/pages/PasswordResetPage.js +34 -0
  120. package/dist/client/pages/PasswordResetPage.js.map +1 -0
  121. package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
  122. package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
  123. package/dist/client/pages/PasswordResetRequestPage.js +27 -0
  124. package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
  125. package/dist/client/pages/PlaceholderPage.d.ts +7 -0
  126. package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
  127. package/dist/client/pages/PlaceholderPage.js +16 -0
  128. package/dist/client/pages/PlaceholderPage.js.map +1 -0
  129. package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
  130. package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
  131. package/dist/client/pages/SuperAdminDashboard.js +123 -0
  132. package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
  133. package/dist/client/theme.d.ts +12 -0
  134. package/dist/client/theme.d.ts.map +1 -0
  135. package/dist/client/theme.js +16 -0
  136. package/dist/client/theme.js.map +1 -0
  137. package/dist/client/types.d.ts +78 -0
  138. package/dist/client/types.d.ts.map +1 -0
  139. package/dist/client/types.js +2 -0
  140. package/dist/client/types.js.map +1 -0
  141. package/dist/index.d.ts +3 -1
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/server/index.d.ts +2 -0
  146. package/dist/server/index.d.ts.map +1 -1
  147. package/dist/server/index.js +1 -0
  148. package/dist/server/index.js.map +1 -1
  149. package/dist/server/org-admin.d.ts +45 -0
  150. package/dist/server/org-admin.d.ts.map +1 -0
  151. package/dist/server/org-admin.js +63 -0
  152. package/dist/server/org-admin.js.map +1 -0
  153. package/dist/server/routes/orgs.routes.d.ts.map +1 -1
  154. package/dist/server/routes/orgs.routes.js +81 -12
  155. package/dist/server/routes/orgs.routes.js.map +1 -1
  156. package/dist/server/types.d.ts +2 -0
  157. package/dist/server/types.d.ts.map +1 -1
  158. package/dist/server/types.js.map +1 -1
  159. package/package.json +18 -11
  160. package/.eslintrc.cjs +0 -18
  161. package/CHANGELOG.md +0 -159
  162. package/src/client/api.ts +0 -314
  163. package/src/client/components/AuditEventRow.tsx +0 -59
  164. package/src/client/components/CascadePreview.tsx +0 -36
  165. package/src/client/components/DangerZoneCard.tsx +0 -103
  166. package/src/client/components/InvitationCodeDisplay.tsx +0 -48
  167. package/src/client/components/InviteForm.tsx +0 -77
  168. package/src/client/components/MemberRow.tsx +0 -69
  169. package/src/client/components/PendingTransferBanner.tsx +0 -98
  170. package/src/client/components/PlaceholderCard.tsx +0 -26
  171. package/src/client/components/RoleBadge.tsx +0 -26
  172. package/src/client/components/RoleSelect.tsx +0 -35
  173. package/src/client/hooks/.gitkeep +0 -0
  174. package/src/client/hooks/useCurrentMembership.ts +0 -24
  175. package/src/client/hooks/useMembers.ts +0 -24
  176. package/src/client/hooks/usePendingInvitations.ts +0 -24
  177. package/src/client/hooks/usePendingTransfer.ts +0 -27
  178. package/src/client/pages/AuditLogPage.tsx +0 -164
  179. package/src/client/pages/EmailChangePage.tsx +0 -144
  180. package/src/client/pages/InvitationAcceptPage.tsx +0 -163
  181. package/src/client/pages/InvitationCodePage.tsx +0 -108
  182. package/src/client/pages/MembersPage.tsx +0 -290
  183. package/src/client/pages/OrgSettingsPage.tsx +0 -185
  184. package/src/client/pages/OwnershipTransferPage.tsx +0 -163
  185. package/src/client/pages/PasswordResetPage.tsx +0 -104
  186. package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
  187. package/src/client/pages/PlaceholderPage.tsx +0 -20
  188. package/src/client/pages/SuperAdminDashboard.tsx +0 -401
  189. package/src/client/types.ts +0 -78
  190. package/src/index.ts +0 -24
  191. package/src/server/crypto.ts +0 -47
  192. package/src/server/index.ts +0 -167
  193. package/src/server/middleware/require-membership.ts +0 -48
  194. package/src/server/middleware/require-role.ts +0 -19
  195. package/src/server/middleware/require-super-admin.ts +0 -32
  196. package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
  197. package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
  198. package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
  199. package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
  200. package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
  201. package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
  202. package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
  203. package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
  204. package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
  205. package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
  206. package/src/server/migrations/0011_seed_super_admin.sql +0 -15
  207. package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
  208. package/src/server/routes/admin.routes.ts +0 -208
  209. package/src/server/routes/audit.routes.ts +0 -93
  210. package/src/server/routes/health.routes.ts +0 -46
  211. package/src/server/routes/invitations.routes.ts +0 -252
  212. package/src/server/routes/me.routes.ts +0 -143
  213. package/src/server/routes/orgs.routes.ts +0 -428
  214. package/src/server/routes/transfer.routes.ts +0 -110
  215. package/src/server/services/.gitkeep +0 -0
  216. package/src/server/services/audit.service.ts +0 -49
  217. package/src/server/services/email-change.service.ts +0 -178
  218. package/src/server/services/invitations.service.ts +0 -316
  219. package/src/server/services/memberships.service.ts +0 -129
  220. package/src/server/services/organizations.service.ts +0 -110
  221. package/src/server/services/ownership.service.ts +0 -170
  222. package/src/server/services/password-reset.service.ts +0 -94
  223. package/src/server/services/super-admin.service.ts +0 -321
  224. package/src/server/sql/.gitkeep +0 -0
  225. package/src/server/types.ts +0 -145
  226. package/src/shared/types.ts +0 -24
  227. package/tests/integration/audit-fires.test.ts +0 -288
  228. package/tests/integration/cascade-preview.test.ts +0 -157
  229. package/tests/integration/email-change.test.ts +0 -190
  230. package/tests/integration/feature-flags.test.ts +0 -213
  231. package/tests/integration/invitations-code.test.ts +0 -218
  232. package/tests/integration/invitations-expiry.test.ts +0 -216
  233. package/tests/integration/invitations-resend.test.ts +0 -241
  234. package/tests/integration/invitations-revoke.test.ts +0 -226
  235. package/tests/integration/invitations-switch-org.test.ts +0 -156
  236. package/tests/integration/invitations-token.test.ts +0 -221
  237. package/tests/integration/migrations.test.ts +0 -119
  238. package/tests/integration/only-owner-protections.test.ts +0 -130
  239. package/tests/integration/org-lifecycle.test.ts +0 -169
  240. package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
  241. package/tests/integration/ownership-transfer-expire.test.ts +0 -171
  242. package/tests/integration/ownership-transfer-happy.test.ts +0 -184
  243. package/tests/integration/ownership-transfer-locks.test.ts +0 -146
  244. package/tests/integration/password-reset.test.ts +0 -200
  245. package/tests/integration/super-admin-actions.test.ts +0 -180
  246. package/tests/integration/super-admin-restrictions.test.ts +0 -209
  247. package/tests/setup/global-setup.ts +0 -20
  248. package/tests/unit/adapter-shape.test.ts +0 -330
  249. package/tests/unit/role-permissions.test.ts +0 -236
  250. package/tests/unit/validation.test.ts +0 -304
  251. package/tsconfig.client.json +0 -13
  252. package/tsconfig.json +0 -12
  253. package/tsconfig.tsbuildinfo +0 -1
  254. package/vitest.config.ts +0 -13
@@ -1,190 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
2
- import { createHash } from 'crypto';
3
- import express from 'express';
4
- import request from 'supertest';
5
- import { Pool } from 'pg';
6
- import { createServerModule } from '../../src/server/index.js';
7
- import type { ServerModuleAdapter, OrgRole } from '../../src/server/types.js';
8
-
9
- const describeWithDb = process.env.DATABASE_URL ? describe : describe.skip;
10
-
11
- function sha256(s: string): string {
12
- return createHash('sha256').update(s).digest('hex');
13
- }
14
-
15
- let currentUserId: number | null = null;
16
- let currentOrgId: number | null = null;
17
-
18
- const sendEmailChangeVerification = vi.fn(async () => {});
19
- const sendEmailChangeOldNotice = vi.fn(async () => {});
20
- const sendEmailChangedFinalNotice = vi.fn(async () => {});
21
- const sendPasswordResetEmail = vi.fn(async () => {});
22
-
23
- const testAdapter: ServerModuleAdapter = {
24
- getCurrentUserId: async () => currentUserId,
25
- getOrganizationIdForUser: async () => currentOrgId,
26
- isUserOrgAdmin: async (userId) => userId <= 2,
27
- logger: { info: () => {}, warn: () => {}, error: () => {} },
28
- getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
29
- getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
30
- findUserByEmail: async (email) => {
31
- const m = email.match(/^u(\d+)@test\.com$/);
32
- return m ? { id: parseInt(m[1]), email } : null;
33
- },
34
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
35
- setUserPassword: async () => {},
36
- hashPassword: async (p) => `h:${p}`,
37
- verifyPassword: async (p, h) => h === `h:${p}`,
38
- invalidateAllUserSessions: async () => {},
39
- sendInviteEmail: async () => {},
40
- sendOwnershipTransferEmail: async () => {},
41
- sendEmailChangeVerification,
42
- sendEmailChangeOldNotice,
43
- sendEmailChangedFinalNotice,
44
- sendPasswordResetEmail,
45
- sendOrgDeletionNotice: async () => {},
46
- emitNotification: async () => {},
47
- };
48
-
49
- async function cleanAll(pool: Pool) {
50
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
51
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
52
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
53
- }
54
-
55
- async function seedOrg(pool: Pool, orgId = 1) {
56
- await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
57
- VALUES (${orgId}, 'Test Org ${orgId}', 'test-org-${orgId}', 1, '{}') ON CONFLICT DO NOTHING`);
58
- await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role) VALUES
59
- (${orgId}, 1, 'owner'), (${orgId}, 2, 'admin'), (${orgId}, 3, 'member'), (${orgId}, 4, 'viewer')
60
- ON CONFLICT DO NOTHING`);
61
- }
62
-
63
- describeWithDb('email change flow', () => {
64
- let pool: Pool;
65
- let app: express.Express;
66
-
67
- beforeAll(async () => {
68
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
69
- const mod = createServerModule({ adapter: testAdapter, pool, features: {} });
70
- app = express();
71
- app.use(express.json());
72
- app.use(mod.router);
73
- });
74
-
75
- afterAll(async () => {
76
- await pool.end();
77
- });
78
-
79
- beforeEach(async () => {
80
- await cleanAll(pool);
81
- sendEmailChangeVerification.mockClear();
82
- sendEmailChangeOldNotice.mockClear();
83
- sendEmailChangedFinalNotice.mockClear();
84
- sendPasswordResetEmail.mockClear();
85
- currentUserId = 3;
86
- currentOrgId = 1;
87
- await seedOrg(pool);
88
- });
89
-
90
- it('POST /me/email-change → 200 and calls verification + notice emails', async () => {
91
- const res = await request(app)
92
- .post('/me/email-change')
93
- .send({ newEmail: 'new@example.com', currentPassword: 'mypassword' });
94
-
95
- expect(res.status).toBe(200);
96
-
97
- // Both verification and old-address notice should be sent
98
- expect(sendEmailChangeVerification).toHaveBeenCalledOnce();
99
- expect(sendEmailChangeOldNotice).toHaveBeenCalledOnce();
100
-
101
- // Verify row in DB
102
- const dbRow = await pool.query(
103
- `SELECT * FROM tm_email_change_requests WHERE user_id = 3 ORDER BY created_at DESC LIMIT 1`
104
- );
105
- expect(dbRow.rows.length).toBe(1);
106
- expect(dbRow.rows[0].new_email).toBe('new@example.com');
107
- // Status is tracked via verified_at/cancelled_at (both null = pending)
108
- expect(dbRow.rows[0].verified_at).toBeNull();
109
- expect(dbRow.rows[0].cancelled_at).toBeNull();
110
- });
111
-
112
- it('GET /me/email-change/verify?token=:token → 200, email changed', async () => {
113
- sendEmailChangeVerification.mockClear();
114
- await request(app)
115
- .post('/me/email-change')
116
- .send({ newEmail: 'verified@example.com', currentPassword: 'mypassword' });
117
-
118
- // Extract raw token from the verifyUrl sent to the spy
119
- const callArg = sendEmailChangeVerification.mock.calls[0]?.[0] as { verifyUrl?: string } | undefined;
120
- const verifyUrl = callArg?.verifyUrl ?? '';
121
- const token = new URLSearchParams(verifyUrl.split('?')[1] ?? '').get('token') ?? '';
122
- expect(token).toBeTruthy();
123
-
124
- // Route requires auth — keep currentUserId=3
125
- const res = await request(app)
126
- .get(`/me/email-change/verify?token=${token}`);
127
-
128
- expect(res.status).toBe(200);
129
- // sendEmailChangedFinalNotice is called twice (for new address and old address)
130
- expect(sendEmailChangedFinalNotice).toHaveBeenCalledTimes(2);
131
-
132
- // Verify DB shows verified_at is set
133
- const tokenHash = sha256(token);
134
- const updated = await pool.query(
135
- `SELECT verified_at FROM tm_email_change_requests WHERE verify_token_hash = $1`,
136
- [tokenHash]
137
- );
138
- expect(updated.rows[0].verified_at).not.toBeNull();
139
- });
140
-
141
- it('GET /me/email-change/cancel?token=:cancelToken → 200, change cancelled', async () => {
142
- sendEmailChangeOldNotice.mockClear();
143
- await request(app)
144
- .post('/me/email-change')
145
- .send({ newEmail: 'cancel@example.com', currentPassword: 'mypassword' });
146
-
147
- // Extract raw cancel token from the cancelUrl sent to the spy
148
- const cancelCallArg = sendEmailChangeOldNotice.mock.calls[0]?.[0] as { cancelUrl?: string } | undefined;
149
- const cancelUrl = cancelCallArg?.cancelUrl ?? '';
150
- const cancelToken = new URLSearchParams(cancelUrl.split('?')[1] ?? '').get('token') ?? '';
151
- expect(cancelToken).toBeTruthy();
152
-
153
- currentUserId = null; // cancel is unauthenticated — token is the credential
154
- const res = await request(app)
155
- .get(`/me/email-change/cancel?token=${cancelToken}`);
156
-
157
- expect(res.status).toBe(200);
158
-
159
- // Verify DB shows cancelled_at is set
160
- const cancelTokenHash = sha256(cancelToken);
161
- const updated = await pool.query(
162
- `SELECT cancelled_at FROM tm_email_change_requests WHERE cancel_token_hash = $1`,
163
- [cancelTokenHash]
164
- );
165
- expect(updated.rows[0].cancelled_at).not.toBeNull();
166
- });
167
-
168
- it.skip('after cancel: sendPasswordResetEmail is triggered (not yet implemented in service)', async () => {
169
- // cancelEmailChange service calls invalidateAllUserSessions but not sendPasswordResetEmail.
170
- // Skipped until the security-triggered password-reset-on-cancel flow is implemented.
171
- });
172
-
173
- it('rate limit: 4th email-change request in 24h → 429', async () => {
174
- // Insert 3 existing requests within the last 24 hours
175
- const past = new Date(Date.now() - 30 * 60 * 1000).toISOString(); // 30 min ago
176
- for (let i = 0; i < 3; i++) {
177
- await pool.query(
178
- `INSERT INTO tm_email_change_requests (user_id, new_email, verify_token_hash, cancel_token_hash, expires_at, created_at)
179
- VALUES (3, $1, $2, $3, NOW() + INTERVAL '24 hours', $4)`,
180
- [`rate${i}@example.com`, sha256(`verify-rate${i}`), sha256(`cancel-rate${i}`), past]
181
- );
182
- }
183
-
184
- const res = await request(app)
185
- .post('/me/email-change')
186
- .send({ newEmail: 'fourth@example.com', currentPassword: 'mypassword' });
187
-
188
- expect(res.status).toBe(429);
189
- });
190
- });
@@ -1,213 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
- import express from 'express';
3
- import request from 'supertest';
4
- import { Pool } from 'pg';
5
- import { createServerModule } from '../../src/server/index.js';
6
- import type { ServerModuleAdapter, OrgRole } from '../../src/server/types.js';
7
-
8
- const describeWithDb = process.env.DATABASE_URL ? describe : describe.skip;
9
-
10
- let currentUserId: number | null = null;
11
- let currentOrgId: number | null = null;
12
-
13
- const testAdapter: ServerModuleAdapter = {
14
- getCurrentUserId: async () => currentUserId,
15
- getOrganizationIdForUser: async () => currentOrgId,
16
- isUserOrgAdmin: async (userId) => userId <= 2,
17
- logger: { info: () => {}, warn: () => {}, error: () => {} },
18
- getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
19
- getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
20
- findUserByEmail: async (email) => {
21
- const m = email.match(/^u(\d+)@test\.com$/);
22
- return m ? { id: parseInt(m[1]), email } : null;
23
- },
24
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
25
- setUserPassword: async () => {},
26
- hashPassword: async (p) => `h:${p}`,
27
- verifyPassword: async (p, h) => h === `h:${p}`,
28
- invalidateAllUserSessions: async () => {},
29
- sendInviteEmail: async () => {},
30
- sendOwnershipTransferEmail: async () => {},
31
- sendEmailChangeVerification: async () => {},
32
- sendEmailChangeOldNotice: async () => {},
33
- sendEmailChangedFinalNotice: async () => {},
34
- sendPasswordResetEmail: async () => {},
35
- sendOrgDeletionNotice: async () => {},
36
- emitNotification: async () => {},
37
- };
38
-
39
- async function cleanAll(pool: Pool) {
40
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
41
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
42
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
43
- }
44
-
45
- async function seedOrg(pool: Pool, orgId = 1) {
46
- await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
47
- VALUES (${orgId}, 'Test Org ${orgId}', 'test-org-${orgId}', 1, '{}') ON CONFLICT DO NOTHING`);
48
- await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role) VALUES
49
- (${orgId}, 1, 'owner'), (${orgId}, 2, 'admin'), (${orgId}, 3, 'member'), (${orgId}, 4, 'viewer')
50
- ON CONFLICT DO NOTHING`);
51
- }
52
-
53
- function buildApp(pool: Pool, features: Record<string, boolean>) {
54
- const mod = createServerModule({ adapter: testAdapter, pool, features });
55
- const app = express();
56
- app.use(express.json());
57
- app.use(mod.router);
58
- return app;
59
- }
60
-
61
- describeWithDb('feature flags', () => {
62
- let pool: Pool;
63
-
64
- beforeAll(async () => {
65
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
66
- });
67
-
68
- afterAll(async () => {
69
- await pool.end();
70
- });
71
-
72
- beforeEach(async () => {
73
- await cleanAll(pool);
74
- currentUserId = 1;
75
- currentOrgId = 1;
76
- await seedOrg(pool);
77
- });
78
-
79
- describe('enableInvites = false', () => {
80
- it('POST /orgs/1/invitations → 501', async () => {
81
- const app = buildApp(pool, { enableInvites: false });
82
- const res = await request(app)
83
- .post('/orgs/1/invitations')
84
- .send({ email: 'test@example.com', role: 'member' });
85
- expect(res.status).toBe(501);
86
- });
87
- });
88
-
89
- describe('enableAuditLog = false', () => {
90
- it('GET /orgs/1/audit → 501', async () => {
91
- const app = buildApp(pool, { enableAuditLog: false });
92
- const res = await request(app).get('/orgs/1/audit');
93
- expect(res.status).toBe(501);
94
- });
95
- });
96
-
97
- describe('enableOwnershipTransfer = false', () => {
98
- it('POST /orgs/1/transfer → 501', async () => {
99
- const app = buildApp(pool, { enableOwnershipTransfer: false });
100
- const res = await request(app)
101
- .post('/orgs/1/transfer')
102
- .send({ toUserId: 2, confirmOrgName: 'Test Org 1' });
103
- expect(res.status).toBe(501);
104
- });
105
- });
106
-
107
- describe('enableEmailChange = false', () => {
108
- it('POST /me/email-change → 501', async () => {
109
- const app = buildApp(pool, { enableEmailChange: false });
110
- const res = await request(app)
111
- .post('/me/email-change')
112
- .send({ newEmail: 'new@example.com', currentPassword: 'pass' });
113
- expect(res.status).toBe(501);
114
- });
115
- });
116
-
117
- describe('enablePasswordReset = false', () => {
118
- it('POST /me/password-reset/request → 501', async () => {
119
- const app = buildApp(pool, { enablePasswordReset: false });
120
- currentUserId = null;
121
- const res = await request(app)
122
- .post('/me/password-reset/request')
123
- .send({ email: 'u1@test.com' });
124
- expect(res.status).toBe(501);
125
- });
126
- });
127
-
128
- describe('enableSuperAdmin = false', () => {
129
- it('GET /admin/orgs → 403, 404, or 501', async () => {
130
- const app = buildApp(pool, { enableSuperAdmin: false });
131
- const res = await request(app).get('/admin/orgs');
132
- // 404 when feature disabled (middleware returns 404 to obscure admin routes)
133
- // 403 if user not super-admin, 501 if feature check first
134
- expect([403, 404, 501]).toContain(res.status);
135
- });
136
- });
137
-
138
- describe('all flags enabled', () => {
139
- it('POST /orgs/1/invitations returns non-501', async () => {
140
- const app = buildApp(pool, {
141
- enableInvites: true,
142
- enableAuditLog: true,
143
- enableOwnershipTransfer: true,
144
- enableEmailChange: true,
145
- enablePasswordReset: true,
146
- enableSuperAdmin: true,
147
- });
148
- const res = await request(app)
149
- .post('/orgs/1/invitations')
150
- .send({ email: 'test@example.com', role: 'member' });
151
- expect(res.status).not.toBe(501);
152
- });
153
-
154
- it('GET /orgs/1/audit returns non-501', async () => {
155
- const app = buildApp(pool, {
156
- enableInvites: true,
157
- enableAuditLog: true,
158
- enableOwnershipTransfer: true,
159
- enableEmailChange: true,
160
- enablePasswordReset: true,
161
- enableSuperAdmin: true,
162
- });
163
- const res = await request(app).get('/orgs/1/audit');
164
- expect(res.status).not.toBe(501);
165
- });
166
-
167
- it('POST /orgs/1/transfer returns non-501', async () => {
168
- const app = buildApp(pool, {
169
- enableInvites: true,
170
- enableAuditLog: true,
171
- enableOwnershipTransfer: true,
172
- enableEmailChange: true,
173
- enablePasswordReset: true,
174
- enableSuperAdmin: true,
175
- });
176
- const res = await request(app)
177
- .post('/orgs/1/transfer')
178
- .send({ toUserId: 2, confirmOrgName: 'Test Org 1' });
179
- expect(res.status).not.toBe(501);
180
- });
181
-
182
- it('POST /me/email-change returns non-501', async () => {
183
- const app = buildApp(pool, {
184
- enableInvites: true,
185
- enableAuditLog: true,
186
- enableOwnershipTransfer: true,
187
- enableEmailChange: true,
188
- enablePasswordReset: true,
189
- enableSuperAdmin: true,
190
- });
191
- const res = await request(app)
192
- .post('/me/email-change')
193
- .send({ newEmail: 'new@example.com', currentPassword: 'pass' });
194
- expect(res.status).not.toBe(501);
195
- });
196
-
197
- it('POST /me/password-reset/request returns non-501', async () => {
198
- const app = buildApp(pool, {
199
- enableInvites: true,
200
- enableAuditLog: true,
201
- enableOwnershipTransfer: true,
202
- enableEmailChange: true,
203
- enablePasswordReset: true,
204
- enableSuperAdmin: true,
205
- });
206
- currentUserId = null;
207
- const res = await request(app)
208
- .post('/me/password-reset/request')
209
- .send({ email: 'u1@test.com' });
210
- expect(res.status).not.toBe(501);
211
- });
212
- });
213
- });
@@ -1,218 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
- import express from 'express';
3
- import request from 'supertest';
4
- import { Pool } from 'pg';
5
- import { createServerModule } from '../../src/server/index.js';
6
- import type { ServerModuleAdapter, OrgRole } from '../../src/server/types.js';
7
-
8
- const DATABASE_URL = process.env.DATABASE_URL;
9
-
10
- describe.skipIf(!DATABASE_URL)('Invitations – Code Accept', () => {
11
- let pool: Pool;
12
- let app: express.Express;
13
- let currentUserId: number | null = null;
14
- let currentOrgId: number | null = null;
15
-
16
- const testAdapter: ServerModuleAdapter = {
17
- getCurrentUserId: async () => currentUserId,
18
- getOrganizationIdForUser: async () => currentOrgId,
19
- isUserOrgAdmin: async (userId, _orgId) => userId === 1 || userId === 2,
20
- logger: { info: () => {}, warn: () => {}, error: () => {} },
21
- getUserById: async (id) => {
22
- const users: Record<number, { id: number; email: string; name: string }> = {
23
- 1: { id: 1, email: 'owner@test.com', name: 'Owner' },
24
- 2: { id: 2, email: 'admin@test.com', name: 'Admin' },
25
- 3: { id: 3, email: 'member@test.com', name: 'Member' },
26
- 4: { id: 4, email: 'viewer@test.com', name: 'Viewer' },
27
- 5: { id: 5, email: 'outsider@test.com', name: 'Outsider' },
28
- };
29
- return users[id] ?? null;
30
- },
31
- getUsersByIds: async (ids) =>
32
- ids.map((id) => ({ id, email: `user${id}@test.com`, name: `User${id}` })),
33
- findUserByEmail: async (email) => {
34
- const map: Record<string, { id: number; email: string }> = {
35
- 'owner@test.com': { id: 1, email: 'owner@test.com' },
36
- 'admin@test.com': { id: 2, email: 'admin@test.com' },
37
- 'member@test.com': { id: 3, email: 'member@test.com' },
38
- 'outsider@test.com': { id: 5, email: 'outsider@test.com' },
39
- 'new@test.com': { id: 6, email: 'new@test.com' },
40
- };
41
- return map[email] ?? null;
42
- },
43
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({
44
- id: 99,
45
- email,
46
- }),
47
- setUserPassword: async () => {},
48
- hashPassword: async (p) => `h:${p}`,
49
- verifyPassword: async (p, h) => h === `h:${p}`,
50
- invalidateAllUserSessions: async () => {},
51
- sendInviteEmail: async () => {},
52
- sendOwnershipTransferEmail: async () => {},
53
- sendEmailChangeVerification: async () => {},
54
- sendEmailChangeOldNotice: async () => {},
55
- sendEmailChangedFinalNotice: async () => {},
56
- sendPasswordResetEmail: async () => {},
57
- sendOrgDeletionNotice: async () => {},
58
- emitNotification: async () => {},
59
- };
60
-
61
- beforeAll(async () => {
62
- pool = new Pool({ connectionString: DATABASE_URL });
63
-
64
- const serverModule = createServerModule({ adapter: testAdapter, pool, features: {
65
- enableInvites: true,
66
- enableAuditLog: false,
67
- } });
68
-
69
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
70
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
71
- await pool.query(
72
- `INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings) VALUES (1, 'Test Org', 'test-org', 1, '{}')`
73
- );
74
- await pool.query(
75
- `INSERT INTO tm_memberships (org_id, user_id, role) VALUES (1, 1, 'owner'), (1, 2, 'admin'), (1, 3, 'member'), (1, 4, 'viewer')`
76
- );
77
-
78
- app = express();
79
- app.use(express.json());
80
- app.use(serverModule.router);
81
- });
82
-
83
- afterAll(async () => {
84
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
85
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
86
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
87
- await pool.end();
88
- });
89
-
90
- beforeEach(async () => {
91
- currentUserId = null;
92
- currentOrgId = null;
93
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
94
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1 AND user_id = 5`);
95
- });
96
-
97
- it('POST /orgs/1/invitations as admin creates invite (201)', async () => {
98
- currentUserId = 2;
99
- currentOrgId = 1;
100
-
101
- const res = await request(app)
102
- .post('/orgs/1/invitations')
103
- .send({ email: 'outsider@test.com', role: 'viewer' });
104
-
105
- expect(res.status).toBe(201);
106
- expect(res.body).toMatchObject({
107
- invitation: expect.objectContaining({
108
- email: 'outsider@test.com',
109
- role: 'viewer',
110
- }),
111
- });
112
- });
113
-
114
- it('GET /orgs/1/invitations/:id/code as admin returns plaintext 6-digit code (200)', async () => {
115
- currentUserId = 2;
116
- currentOrgId = 1;
117
-
118
- const createRes = await request(app)
119
- .post('/orgs/1/invitations')
120
- .send({ email: 'outsider@test.com', role: 'viewer' });
121
- expect(createRes.status).toBe(201);
122
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
123
-
124
- const res = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
125
- expect(res.status).toBe(200);
126
- expect(res.body.code).toBeDefined();
127
- expect(/^\d{6}$/.test(String(res.body.code))).toBe(true);
128
- });
129
-
130
- it('GET /orgs/1/invitations/:id/code as member → 403', async () => {
131
- currentUserId = 2;
132
- currentOrgId = 1;
133
-
134
- const createRes = await request(app)
135
- .post('/orgs/1/invitations')
136
- .send({ email: 'outsider@test.com', role: 'viewer' });
137
- expect(createRes.status).toBe(201);
138
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
139
-
140
- currentUserId = 3;
141
- currentOrgId = 1;
142
-
143
- const res = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
144
- expect(res.status).toBe(403);
145
- });
146
-
147
- it('POST /invitations/accept/code with valid code and matching email → 200', async () => {
148
- currentUserId = 2;
149
- currentOrgId = 1;
150
-
151
- const createRes = await request(app)
152
- .post('/orgs/1/invitations')
153
- .send({ email: 'outsider@test.com', role: 'member' });
154
- expect(createRes.status).toBe(201);
155
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
156
-
157
- const codeRes = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
158
- expect(codeRes.status).toBe(200);
159
- const code = codeRes.body.code;
160
-
161
- currentUserId = 5;
162
- currentOrgId = null;
163
-
164
- const res = await request(app)
165
- .post('/invitations/accept/code')
166
- .send({ email: 'outsider@test.com', code });
167
-
168
- expect(res.status).toBe(200);
169
-
170
- const membershipRow = await pool.query(
171
- `SELECT * FROM tm_memberships WHERE org_id = 1 AND user_id = 5`
172
- );
173
- expect(membershipRow.rows.length).toBe(1);
174
- expect(membershipRow.rows[0].role).toBe('member');
175
- });
176
-
177
- it('POST /invitations/accept/code with wrong code → 400 or 404', async () => {
178
- currentUserId = 2;
179
- currentOrgId = 1;
180
-
181
- const createRes = await request(app)
182
- .post('/orgs/1/invitations')
183
- .send({ email: 'outsider@test.com', role: 'member' });
184
- expect(createRes.status).toBe(201);
185
-
186
- currentUserId = 5;
187
- currentOrgId = null;
188
-
189
- const res = await request(app)
190
- .post('/invitations/accept/code')
191
- .send({ email: 'outsider@test.com', code: '000000' });
192
-
193
- expect([400, 404]).toContain(res.status);
194
- });
195
-
196
- it('POST /invitations/accept/code with wrong email → 400 or 404', async () => {
197
- currentUserId = 2;
198
- currentOrgId = 1;
199
-
200
- const createRes = await request(app)
201
- .post('/orgs/1/invitations')
202
- .send({ email: 'outsider@test.com', role: 'member' });
203
- expect(createRes.status).toBe(201);
204
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
205
-
206
- const codeRes = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
207
- const code = codeRes.body.code;
208
-
209
- currentUserId = null;
210
- currentOrgId = null;
211
-
212
- const res = await request(app)
213
- .post('/invitations/accept/code')
214
- .send({ email: 'wrongperson@test.com', code });
215
-
216
- expect([400, 404]).toContain(res.status);
217
- });
218
- });