@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,216 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } 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
- function extractToken(spy: ReturnType<typeof vi.fn>): string {
11
- const callArg = spy.mock.calls[0]?.[0] as { magicLinkUrl?: string } | undefined;
12
- const url = callArg?.magicLinkUrl ?? '';
13
- const qs = url.includes('?') ? url.split('?')[1] : '';
14
- return new URLSearchParams(qs).get('token') ?? '';
15
- }
16
-
17
- describe.skipIf(!DATABASE_URL)('Invitations – Expiry', () => {
18
- let pool: Pool;
19
- let app: express.Express;
20
- let currentUserId: number | null = null;
21
- let currentOrgId: number | null = null;
22
- const sendInviteEmail = vi.fn(async () => {});
23
-
24
- const testAdapter: ServerModuleAdapter = {
25
- getCurrentUserId: async () => currentUserId,
26
- getOrganizationIdForUser: async () => currentOrgId,
27
- isUserOrgAdmin: async (userId, _orgId) => userId === 1 || userId === 2,
28
- logger: { info: () => {}, warn: () => {}, error: () => {} },
29
- getUserById: async (id) => {
30
- const users: Record<number, { id: number; email: string; name: string }> = {
31
- 1: { id: 1, email: 'owner@test.com', name: 'Owner' },
32
- 2: { id: 2, email: 'admin@test.com', name: 'Admin' },
33
- 3: { id: 3, email: 'member@test.com', name: 'Member' },
34
- 4: { id: 4, email: 'viewer@test.com', name: 'Viewer' },
35
- 5: { id: 5, email: 'outsider@test.com', name: 'Outsider' },
36
- };
37
- return users[id] ?? null;
38
- },
39
- getUsersByIds: async (ids) =>
40
- ids.map((id) => ({ id, email: `user${id}@test.com`, name: `User${id}` })),
41
- findUserByEmail: async (email) => {
42
- const map: Record<string, { id: number; email: string }> = {
43
- 'owner@test.com': { id: 1, email: 'owner@test.com' },
44
- 'admin@test.com': { id: 2, email: 'admin@test.com' },
45
- 'member@test.com': { id: 3, email: 'member@test.com' },
46
- 'outsider@test.com': { id: 5, email: 'outsider@test.com' },
47
- 'new@test.com': { id: 6, email: 'new@test.com' },
48
- };
49
- return map[email] ?? null;
50
- },
51
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({
52
- id: 99,
53
- email,
54
- }),
55
- setUserPassword: async () => {},
56
- hashPassword: async (p) => `h:${p}`,
57
- verifyPassword: async (p, h) => h === `h:${p}`,
58
- invalidateAllUserSessions: async () => {},
59
- sendInviteEmail,
60
- sendOwnershipTransferEmail: async () => {},
61
- sendEmailChangeVerification: async () => {},
62
- sendEmailChangeOldNotice: async () => {},
63
- sendEmailChangedFinalNotice: async () => {},
64
- sendPasswordResetEmail: async () => {},
65
- sendOrgDeletionNotice: async () => {},
66
- emitNotification: async () => {},
67
- };
68
-
69
- beforeAll(async () => {
70
- pool = new Pool({ connectionString: DATABASE_URL });
71
-
72
- const serverModule = createServerModule({ adapter: testAdapter, pool, features: {
73
- enableInvites: true,
74
- enableAuditLog: false,
75
- } });
76
-
77
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
78
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
79
- await pool.query(
80
- `INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings) VALUES (1, 'Test Org', 'test-org', 1, '{}')`
81
- );
82
- await pool.query(
83
- `INSERT INTO tm_memberships (org_id, user_id, role) VALUES (1, 1, 'owner'), (1, 2, 'admin'), (1, 3, 'member'), (1, 4, 'viewer')`
84
- );
85
-
86
- app = express();
87
- app.use(express.json());
88
- app.use(serverModule.router);
89
- });
90
-
91
- afterAll(async () => {
92
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
93
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
94
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
95
- await pool.end();
96
- });
97
-
98
- beforeEach(async () => {
99
- currentUserId = null;
100
- currentOrgId = null;
101
- sendInviteEmail.mockClear();
102
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
103
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1 AND user_id = 5`);
104
- });
105
-
106
- it('Accepting a token where expires_at is in the past → 410 or 400', async () => {
107
- currentUserId = 1;
108
- currentOrgId = 1;
109
-
110
- const createRes = await request(app)
111
- .post('/orgs/1/invitations')
112
- .send({ email: 'outsider@test.com', role: 'member' });
113
- expect(createRes.status).toBe(201);
114
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
115
-
116
- const token = extractToken(sendInviteEmail);
117
- expect(token).toBeTruthy();
118
-
119
- await pool.query(
120
- `UPDATE tm_invitations SET expires_at = NOW() - INTERVAL '1 day' WHERE id = $1`,
121
- [inviteId]
122
- );
123
-
124
- currentUserId = 5;
125
- currentOrgId = null;
126
-
127
- const res = await request(app)
128
- .post('/invitations/accept/token')
129
- .send({ token });
130
-
131
- expect([400, 404, 410]).toContain(res.status);
132
- });
133
-
134
- it('Accepting a code where expires_at is in the past → 410 or 400', async () => {
135
- currentUserId = 1;
136
- currentOrgId = 1;
137
-
138
- const createRes = await request(app)
139
- .post('/orgs/1/invitations')
140
- .send({ email: 'outsider@test.com', role: 'member' });
141
- expect(createRes.status).toBe(201);
142
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
143
-
144
- const codeRes = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
145
- expect(codeRes.status).toBe(200);
146
- const code = codeRes.body.code;
147
-
148
- await pool.query(
149
- `UPDATE tm_invitations SET expires_at = NOW() - INTERVAL '1 day' WHERE id = $1`,
150
- [inviteId]
151
- );
152
-
153
- currentUserId = 5;
154
- currentOrgId = null;
155
-
156
- const res = await request(app)
157
- .post('/invitations/accept/code')
158
- .send({ email: 'outsider@test.com', code });
159
-
160
- expect([400, 404, 410]).toContain(res.status);
161
- });
162
-
163
- it('Expired invite appears as expired in invitation list', async () => {
164
- currentUserId = 1;
165
- currentOrgId = 1;
166
-
167
- const createRes = await request(app)
168
- .post('/orgs/1/invitations')
169
- .send({ email: 'outsider@test.com', role: 'member' });
170
- expect(createRes.status).toBe(201);
171
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
172
-
173
- await pool.query(
174
- `UPDATE tm_invitations SET expires_at = NOW() - INTERVAL '1 day' WHERE id = $1`,
175
- [inviteId]
176
- );
177
-
178
- // Expired invites are excluded from the pending list — that's acceptable behaviour
179
- const listRes = await request(app).get('/orgs/1/invitations');
180
- expect(listRes.status).toBe(200);
181
-
182
- const invitations = listRes.body.invitations ?? listRes.body;
183
- expect(Array.isArray(invitations)).toBe(true);
184
- // Expired invites are filtered out from the pending list — not found is acceptable
185
- const found = Array.isArray(invitations)
186
- ? invitations.find((inv: { id: number }) => inv.id === inviteId)
187
- : null;
188
- // Either the expired invite is not in the list (correct behavior), or it has a status indicator
189
- if (found) {
190
- expect(['expired', 'pending'].includes(found.status) || found.isExpired === true).toBe(true);
191
- }
192
- // Not found is the expected behavior (pending list filters out expired invites)
193
- });
194
-
195
- it('Non-expired invite is in the pending list', async () => {
196
- currentUserId = 1;
197
- currentOrgId = 1;
198
-
199
- const createRes = await request(app)
200
- .post('/orgs/1/invitations')
201
- .send({ email: 'outsider@test.com', role: 'member' });
202
- expect(createRes.status).toBe(201);
203
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
204
-
205
- const listRes = await request(app).get('/orgs/1/invitations');
206
- expect(listRes.status).toBe(200);
207
-
208
- const invitations = listRes.body.invitations ?? listRes.body;
209
- const found = Array.isArray(invitations)
210
- ? invitations.find((inv: { id: number }) => inv.id === inviteId)
211
- : null;
212
- expect(found).toBeDefined();
213
- // The pending list returns invitations with status: 'pending'
214
- expect(found?.status).toBe('pending');
215
- });
216
- });
@@ -1,241 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } 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
- function extractToken(spy: ReturnType<typeof vi.fn>): string {
11
- const callArg = spy.mock.calls[0]?.[0] as { magicLinkUrl?: string } | undefined;
12
- const url = callArg?.magicLinkUrl ?? '';
13
- const qs = url.includes('?') ? url.split('?')[1] : '';
14
- return new URLSearchParams(qs).get('token') ?? '';
15
- }
16
-
17
- describe.skipIf(!DATABASE_URL)('Invitations – Resend', () => {
18
- let pool: Pool;
19
- let app: express.Express;
20
- let currentUserId: number | null = null;
21
- let currentOrgId: number | null = null;
22
- const sendInviteEmail = vi.fn(async () => {});
23
-
24
- const testAdapter: ServerModuleAdapter = {
25
- getCurrentUserId: async () => currentUserId,
26
- getOrganizationIdForUser: async () => currentOrgId,
27
- isUserOrgAdmin: async (userId, _orgId) => userId === 1 || userId === 2,
28
- logger: { info: () => {}, warn: () => {}, error: () => {} },
29
- getUserById: async (id) => {
30
- const users: Record<number, { id: number; email: string; name: string }> = {
31
- 1: { id: 1, email: 'owner@test.com', name: 'Owner' },
32
- 2: { id: 2, email: 'admin@test.com', name: 'Admin' },
33
- 3: { id: 3, email: 'member@test.com', name: 'Member' },
34
- 4: { id: 4, email: 'viewer@test.com', name: 'Viewer' },
35
- 5: { id: 5, email: 'outsider@test.com', name: 'Outsider' },
36
- };
37
- return users[id] ?? null;
38
- },
39
- getUsersByIds: async (ids) =>
40
- ids.map((id) => ({ id, email: `user${id}@test.com`, name: `User${id}` })),
41
- findUserByEmail: async (email) => {
42
- const map: Record<string, { id: number; email: string }> = {
43
- 'owner@test.com': { id: 1, email: 'owner@test.com' },
44
- 'admin@test.com': { id: 2, email: 'admin@test.com' },
45
- 'member@test.com': { id: 3, email: 'member@test.com' },
46
- 'outsider@test.com': { id: 5, email: 'outsider@test.com' },
47
- 'new@test.com': { id: 6, email: 'new@test.com' },
48
- };
49
- return map[email] ?? null;
50
- },
51
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({
52
- id: 99,
53
- email,
54
- }),
55
- setUserPassword: async () => {},
56
- hashPassword: async (p) => `h:${p}`,
57
- verifyPassword: async (p, h) => h === `h:${p}`,
58
- invalidateAllUserSessions: async () => {},
59
- sendInviteEmail,
60
- sendOwnershipTransferEmail: async () => {},
61
- sendEmailChangeVerification: async () => {},
62
- sendEmailChangeOldNotice: async () => {},
63
- sendEmailChangedFinalNotice: async () => {},
64
- sendPasswordResetEmail: async () => {},
65
- sendOrgDeletionNotice: async () => {},
66
- emitNotification: async () => {},
67
- };
68
-
69
- beforeAll(async () => {
70
- pool = new Pool({ connectionString: DATABASE_URL });
71
-
72
- const serverModule = createServerModule({ adapter: testAdapter, pool, features: {
73
- enableInvites: true,
74
- enableAuditLog: false,
75
- } });
76
-
77
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
78
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
79
- await pool.query(
80
- `INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings) VALUES (1, 'Test Org', 'test-org', 1, '{}')`
81
- );
82
- await pool.query(
83
- `INSERT INTO tm_memberships (org_id, user_id, role) VALUES (1, 1, 'owner'), (1, 2, 'admin'), (1, 3, 'member'), (1, 4, 'viewer')`
84
- );
85
-
86
- app = express();
87
- app.use(express.json());
88
- app.use(serverModule.router);
89
- });
90
-
91
- afterAll(async () => {
92
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
93
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
94
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
95
- await pool.end();
96
- });
97
-
98
- beforeEach(async () => {
99
- currentUserId = null;
100
- currentOrgId = null;
101
- sendInviteEmail.mockClear();
102
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
103
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1 AND user_id = 5`);
104
- });
105
-
106
- it('POST /orgs/1/invitations/:id/resend as admin → 200', async () => {
107
- currentUserId = 2;
108
- currentOrgId = 1;
109
-
110
- const createRes = await request(app)
111
- .post('/orgs/1/invitations')
112
- .send({ email: 'outsider@test.com', role: 'member' });
113
- expect(createRes.status).toBe(201);
114
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
115
-
116
- const resendRes = await request(app).post(`/orgs/1/invitations/${inviteId}/resend`);
117
- expect(resendRes.status).toBe(200);
118
- });
119
-
120
- it('Old token is invalidated after resend — using old token → 404 or 400', async () => {
121
- currentUserId = 2;
122
- currentOrgId = 1;
123
-
124
- // Create and capture original token
125
- const createRes = await request(app)
126
- .post('/orgs/1/invitations')
127
- .send({ email: 'outsider@test.com', role: 'member' });
128
- expect(createRes.status).toBe(201);
129
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
130
- const oldToken = extractToken(sendInviteEmail);
131
- expect(oldToken).toBeTruthy();
132
-
133
- sendInviteEmail.mockClear();
134
-
135
- // Resend — should regenerate token
136
- const resendRes = await request(app).post(`/orgs/1/invitations/${inviteId}/resend`);
137
- expect(resendRes.status).toBe(200);
138
-
139
- const newToken = extractToken(sendInviteEmail);
140
- expect(newToken).toBeTruthy();
141
-
142
- if (newToken !== oldToken) {
143
- // Token was rotated — old token should fail
144
- currentUserId = 5;
145
- currentOrgId = null;
146
-
147
- const acceptRes = await request(app)
148
- .post('/invitations/accept/token')
149
- .send({ token: oldToken });
150
-
151
- expect([400, 404, 410]).toContain(acceptRes.status);
152
- } else {
153
- // Token not rotated — new token still works
154
- currentUserId = 5;
155
- currentOrgId = null;
156
-
157
- const acceptRes = await request(app)
158
- .post('/invitations/accept/token')
159
- .send({ token: newToken });
160
-
161
- expect(acceptRes.status).toBe(200);
162
- }
163
- });
164
-
165
- it('New token after resend is valid and can be accepted', async () => {
166
- currentUserId = 2;
167
- currentOrgId = 1;
168
-
169
- const createRes = await request(app)
170
- .post('/orgs/1/invitations')
171
- .send({ email: 'outsider@test.com', role: 'member' });
172
- expect(createRes.status).toBe(201);
173
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
174
-
175
- sendInviteEmail.mockClear();
176
-
177
- const resendRes = await request(app).post(`/orgs/1/invitations/${inviteId}/resend`);
178
- expect(resendRes.status).toBe(200);
179
-
180
- const newToken = extractToken(sendInviteEmail);
181
- expect(newToken).toBeTruthy();
182
-
183
- currentUserId = 5;
184
- currentOrgId = null;
185
-
186
- const acceptRes = await request(app)
187
- .post('/invitations/accept/token')
188
- .send({ token: newToken });
189
-
190
- expect(acceptRes.status).toBe(200);
191
-
192
- const membershipRow = await pool.query(
193
- `SELECT * FROM tm_memberships WHERE org_id = 1 AND user_id = 5`
194
- );
195
- expect(membershipRow.rows.length).toBe(1);
196
- });
197
-
198
- it('New code after resend is valid', async () => {
199
- currentUserId = 2;
200
- currentOrgId = 1;
201
-
202
- const createRes = await request(app)
203
- .post('/orgs/1/invitations')
204
- .send({ email: 'outsider@test.com', role: 'member' });
205
- expect(createRes.status).toBe(201);
206
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
207
-
208
- await request(app).post(`/orgs/1/invitations/${inviteId}/resend`);
209
-
210
- const codeRes = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
211
- expect(codeRes.status).toBe(200);
212
- const newCode = codeRes.body.code;
213
- expect(/^\d{6}$/.test(String(newCode))).toBe(true);
214
-
215
- currentUserId = 5;
216
- currentOrgId = null;
217
-
218
- const acceptRes = await request(app)
219
- .post('/invitations/accept/code')
220
- .send({ email: 'outsider@test.com', code: newCode });
221
-
222
- expect(acceptRes.status).toBe(200);
223
- });
224
-
225
- it('POST /orgs/1/invitations/:id/resend as member (non-admin) → 403', async () => {
226
- currentUserId = 2;
227
- currentOrgId = 1;
228
-
229
- const createRes = await request(app)
230
- .post('/orgs/1/invitations')
231
- .send({ email: 'outsider@test.com', role: 'member' });
232
- expect(createRes.status).toBe(201);
233
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
234
-
235
- currentUserId = 3;
236
- currentOrgId = 1;
237
-
238
- const resendRes = await request(app).post(`/orgs/1/invitations/${inviteId}/resend`);
239
- expect(resendRes.status).toBe(403);
240
- });
241
- });
@@ -1,226 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } 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
- function extractToken(spy: ReturnType<typeof vi.fn>): string {
11
- const callArg = spy.mock.calls[0]?.[0] as { magicLinkUrl?: string } | undefined;
12
- const url = callArg?.magicLinkUrl ?? '';
13
- const qs = url.includes('?') ? url.split('?')[1] : '';
14
- return new URLSearchParams(qs).get('token') ?? '';
15
- }
16
-
17
- describe.skipIf(!DATABASE_URL)('Invitations – Revoke', () => {
18
- let pool: Pool;
19
- let app: express.Express;
20
- let currentUserId: number | null = null;
21
- let currentOrgId: number | null = null;
22
- const sendInviteEmail = vi.fn(async () => {});
23
-
24
- const testAdapter: ServerModuleAdapter = {
25
- getCurrentUserId: async () => currentUserId,
26
- getOrganizationIdForUser: async () => currentOrgId,
27
- isUserOrgAdmin: async (userId, _orgId) => userId === 1 || userId === 2,
28
- logger: { info: () => {}, warn: () => {}, error: () => {} },
29
- getUserById: async (id) => {
30
- const users: Record<number, { id: number; email: string; name: string }> = {
31
- 1: { id: 1, email: 'owner@test.com', name: 'Owner' },
32
- 2: { id: 2, email: 'admin@test.com', name: 'Admin' },
33
- 3: { id: 3, email: 'member@test.com', name: 'Member' },
34
- 4: { id: 4, email: 'viewer@test.com', name: 'Viewer' },
35
- 5: { id: 5, email: 'outsider@test.com', name: 'Outsider' },
36
- };
37
- return users[id] ?? null;
38
- },
39
- getUsersByIds: async (ids) =>
40
- ids.map((id) => ({ id, email: `user${id}@test.com`, name: `User${id}` })),
41
- findUserByEmail: async (email) => {
42
- const map: Record<string, { id: number; email: string }> = {
43
- 'owner@test.com': { id: 1, email: 'owner@test.com' },
44
- 'admin@test.com': { id: 2, email: 'admin@test.com' },
45
- 'member@test.com': { id: 3, email: 'member@test.com' },
46
- 'outsider@test.com': { id: 5, email: 'outsider@test.com' },
47
- 'new@test.com': { id: 6, email: 'new@test.com' },
48
- };
49
- return map[email] ?? null;
50
- },
51
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({
52
- id: 99,
53
- email,
54
- }),
55
- setUserPassword: async () => {},
56
- hashPassword: async (p) => `h:${p}`,
57
- verifyPassword: async (p, h) => h === `h:${p}`,
58
- invalidateAllUserSessions: async () => {},
59
- sendInviteEmail,
60
- sendOwnershipTransferEmail: async () => {},
61
- sendEmailChangeVerification: async () => {},
62
- sendEmailChangeOldNotice: async () => {},
63
- sendEmailChangedFinalNotice: async () => {},
64
- sendPasswordResetEmail: async () => {},
65
- sendOrgDeletionNotice: async () => {},
66
- emitNotification: async () => {},
67
- };
68
-
69
- beforeAll(async () => {
70
- pool = new Pool({ connectionString: DATABASE_URL });
71
-
72
- const serverModule = createServerModule({ adapter: testAdapter, pool, features: {
73
- enableInvites: true,
74
- enableAuditLog: false,
75
- } });
76
-
77
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
78
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
79
- await pool.query(
80
- `INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings) VALUES (1, 'Test Org', 'test-org', 1, '{}')`
81
- );
82
- await pool.query(
83
- `INSERT INTO tm_memberships (org_id, user_id, role) VALUES (1, 1, 'owner'), (1, 2, 'admin'), (1, 3, 'member'), (1, 4, 'viewer')`
84
- );
85
-
86
- app = express();
87
- app.use(express.json());
88
- app.use(serverModule.router);
89
- });
90
-
91
- afterAll(async () => {
92
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
93
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1`);
94
- await pool.query(`DELETE FROM tm_organizations WHERE id = 1`);
95
- await pool.end();
96
- });
97
-
98
- beforeEach(async () => {
99
- currentUserId = null;
100
- currentOrgId = null;
101
- sendInviteEmail.mockClear();
102
- await pool.query(`DELETE FROM tm_invitations WHERE org_id = 1`);
103
- await pool.query(`DELETE FROM tm_memberships WHERE org_id = 1 AND user_id = 5`);
104
- });
105
-
106
- it('POST /orgs/1/invitations as admin creates invite (201)', async () => {
107
- currentUserId = 2;
108
- currentOrgId = 1;
109
-
110
- const res = await request(app)
111
- .post('/orgs/1/invitations')
112
- .send({ email: 'outsider@test.com', role: 'member' });
113
-
114
- expect(res.status).toBe(201);
115
- const id = res.body.invitation?.id ?? res.body.id;
116
- expect(id).toBeDefined();
117
- });
118
-
119
- it('DELETE /orgs/1/invitations/:id as admin → 200', async () => {
120
- currentUserId = 2;
121
- currentOrgId = 1;
122
-
123
- const createRes = await request(app)
124
- .post('/orgs/1/invitations')
125
- .send({ email: 'outsider@test.com', role: 'member' });
126
- expect(createRes.status).toBe(201);
127
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
128
-
129
- const deleteRes = await request(app).delete(`/orgs/1/invitations/${inviteId}`);
130
- expect(deleteRes.status).toBe(200);
131
- });
132
-
133
- it('Accepting a revoked invite by token → 400 or 410', async () => {
134
- currentUserId = 2;
135
- currentOrgId = 1;
136
-
137
- const createRes = await request(app)
138
- .post('/orgs/1/invitations')
139
- .send({ email: 'outsider@test.com', role: 'member' });
140
- expect(createRes.status).toBe(201);
141
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
142
-
143
- const token = extractToken(sendInviteEmail);
144
- expect(token).toBeTruthy();
145
-
146
- const deleteRes = await request(app).delete(`/orgs/1/invitations/${inviteId}`);
147
- expect(deleteRes.status).toBe(200);
148
-
149
- currentUserId = 5;
150
- currentOrgId = null;
151
-
152
- const acceptRes = await request(app)
153
- .post('/invitations/accept/token')
154
- .send({ token });
155
-
156
- expect([400, 404, 410]).toContain(acceptRes.status);
157
- });
158
-
159
- it('Accepting a revoked invite by code → 400 or 410', async () => {
160
- currentUserId = 2;
161
- currentOrgId = 1;
162
-
163
- const createRes = await request(app)
164
- .post('/orgs/1/invitations')
165
- .send({ email: 'outsider@test.com', role: 'member' });
166
- expect(createRes.status).toBe(201);
167
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
168
-
169
- const codeRes = await request(app).get(`/orgs/1/invitations/${inviteId}/code`);
170
- expect(codeRes.status).toBe(200);
171
- const code = codeRes.body.code;
172
-
173
- const deleteRes = await request(app).delete(`/orgs/1/invitations/${inviteId}`);
174
- expect(deleteRes.status).toBe(200);
175
-
176
- currentUserId = 5;
177
- currentOrgId = null;
178
-
179
- const acceptRes = await request(app)
180
- .post('/invitations/accept/code')
181
- .send({ email: 'outsider@test.com', code });
182
-
183
- expect([400, 404, 410]).toContain(acceptRes.status);
184
- });
185
-
186
- it('DELETE /orgs/1/invitations/:id as member (non-admin) → 403', async () => {
187
- currentUserId = 2;
188
- currentOrgId = 1;
189
-
190
- const createRes = await request(app)
191
- .post('/orgs/1/invitations')
192
- .send({ email: 'outsider@test.com', role: 'member' });
193
- expect(createRes.status).toBe(201);
194
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
195
-
196
- currentUserId = 3;
197
- currentOrgId = 1;
198
-
199
- const deleteRes = await request(app).delete(`/orgs/1/invitations/${inviteId}`);
200
- expect(deleteRes.status).toBe(403);
201
- });
202
-
203
- it('Revoked invite no longer appears as pending in list', async () => {
204
- currentUserId = 2;
205
- currentOrgId = 1;
206
-
207
- const createRes = await request(app)
208
- .post('/orgs/1/invitations')
209
- .send({ email: 'outsider@test.com', role: 'member' });
210
- expect(createRes.status).toBe(201);
211
- const inviteId = createRes.body.invitation?.id ?? createRes.body.id;
212
-
213
- await request(app).delete(`/orgs/1/invitations/${inviteId}`);
214
-
215
- const listRes = await request(app).get('/orgs/1/invitations');
216
- expect(listRes.status).toBe(200);
217
-
218
- const invitations = listRes.body.invitations ?? listRes.body;
219
- const found = Array.isArray(invitations)
220
- ? invitations.find((inv: { id: number }) => inv.id === inviteId)
221
- : null;
222
- if (found) {
223
- expect(['revoked', 'cancelled', 'deleted']).toContain(found.status);
224
- }
225
- });
226
- });