@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,209 +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
- const describeWithDb = DATABASE_URL ? describe : describe.skip;
10
-
11
- let currentUserId: number | null = 1;
12
- let currentOrgId: number | null = 1;
13
-
14
- const testAdapter: ServerModuleAdapter = {
15
- getCurrentUserId: async () => currentUserId,
16
- getOrganizationIdForUser: async () => currentOrgId,
17
- isUserOrgAdmin: async (userId) => userId <= 2,
18
- logger: { info: () => {}, warn: () => {}, error: () => {} },
19
- getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
20
- getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
21
- findUserByEmail: async (email) => {
22
- const m = email.match(/^u(\d+)@test\.com$/);
23
- return m ? { id: parseInt(m[1]), email } : null;
24
- },
25
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
26
- setUserPassword: async () => {},
27
- hashPassword: async (p) => `h:${p}`,
28
- verifyPassword: async (p, h) => h === `h:${p}`,
29
- invalidateAllUserSessions: async () => {},
30
- sendInviteEmail: vi.fn().mockResolvedValue(undefined),
31
- sendOwnershipTransferEmail: async () => {},
32
- sendEmailChangeVerification: async () => {},
33
- sendEmailChangeOldNotice: async () => {},
34
- sendEmailChangedFinalNotice: async () => {},
35
- sendPasswordResetEmail: vi.fn().mockResolvedValue(undefined),
36
- sendOrgDeletionNotice: async () => {},
37
- emitNotification: async () => {},
38
- };
39
-
40
- describeWithDb('super-admin restrictions (integration)', () => {
41
- let pool: Pool;
42
- let app: express.Express;
43
-
44
- beforeAll(async () => {
45
- pool = new Pool({ connectionString: DATABASE_URL });
46
- const tm = createServerModule({
47
- adapter: testAdapter,
48
- db: pool,
49
- config: {
50
- featureFlags: {
51
- enableInvites: true,
52
- enableAuditLog: true,
53
- enableSuperAdmin: true,
54
- enableOwnershipTransfer: true,
55
- enableEmailChange: true,
56
- enablePasswordReset: true,
57
- },
58
- },
59
- });
60
- await tm.runMigrations();
61
- app = express();
62
- app.use(express.json());
63
- app.use('/api/team', tm.router);
64
- });
65
-
66
- beforeEach(async () => {
67
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
68
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
69
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
70
- // Seed org + super-admin (user 1 is super-admin)
71
- await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
72
- VALUES (1, 'Test Org', 'test-org', 1, '{}')`);
73
- await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role)
74
- VALUES (1,1,'owner'),(1,2,'admin'),(1,3,'member'),(1,4,'viewer')`);
75
- await pool.query(`INSERT INTO tm_super_admins (user_id, granted_by_user_id)
76
- VALUES (1, 1)`);
77
- currentUserId = 1;
78
- currentOrgId = 1;
79
- });
80
-
81
- afterAll(async () => {
82
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
83
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
84
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
85
- await pool.end();
86
- });
87
-
88
- it('super-admin cannot impersonate users (no login-as endpoint exists)', async () => {
89
- // There should be no /admin/users/:id/impersonate or /admin/login-as endpoint
90
- const res = await request(app)
91
- .post('/api/team/admin/users/3/impersonate')
92
- .set('Accept', 'application/json');
93
- // 404 = route doesn't exist (correct — impersonation is banned by spec)
94
- expect(res.status).toBe(404);
95
- });
96
-
97
- it('super-admin cannot set user passwords directly (only trigger reset link)', async () => {
98
- // There should be no /admin/users/:id/set-password endpoint
99
- const res = await request(app)
100
- .post('/api/team/admin/users/3/set-password')
101
- .send({ password: 'newpassword123' })
102
- .set('Accept', 'application/json');
103
- // 404 = route doesn't exist
104
- expect(res.status).toBe(404);
105
- });
106
-
107
- it('password-reset via admin triggers email but adapter.setUserPassword is NOT called directly', async () => {
108
- vi.mocked(testAdapter.sendPasswordResetEmail).mockClear();
109
- const res = await request(app)
110
- .post('/api/team/admin/users/3/password-reset')
111
- .send({ reason: 'user requested support' })
112
- .set('Accept', 'application/json');
113
-
114
- expect([200, 201]).toContain(res.status);
115
- // sendPasswordResetEmail SHOULD be called (sends link to user)
116
- expect(vi.mocked(testAdapter.sendPasswordResetEmail)).toHaveBeenCalledTimes(1);
117
- // But the password reset should go to the USER's email, not be set directly
118
- const callArg = vi.mocked(testAdapter.sendPasswordResetEmail).mock.calls[0]?.[0];
119
- expect(callArg).toBeDefined();
120
- expect(callArg?.to).toMatch(/@test\.com$/); // goes to user's email
121
- });
122
-
123
- it('super-admin cannot access product data beyond org+member info', async () => {
124
- // There should be no /admin/orgs/:id/invoices, /admin/orgs/:id/projects, etc.
125
- const productDataRoutes = [
126
- '/api/team/admin/orgs/1/invoices',
127
- '/api/team/admin/orgs/1/projects',
128
- '/api/team/admin/orgs/1/pay-apps',
129
- '/api/team/admin/orgs/1/documents',
130
- ];
131
- for (const route of productDataRoutes) {
132
- const res = await request(app).get(route).set('Accept', 'application/json');
133
- // 404 = route doesn't exist (product data is out of scope for this module)
134
- expect(res.status).toBe(404);
135
- }
136
- });
137
-
138
- it('super-admin actions require a reason text (cannot omit reason)', async () => {
139
- // appoint-owner without reason → 400
140
- const resNoReason = await request(app)
141
- .post('/api/team/admin/orgs/1/appoint-owner')
142
- .send({ target_user_id: 2 }) // missing reason
143
- .set('Accept', 'application/json');
144
- expect(resNoReason.status).toBe(400);
145
-
146
- // lock without reason → 400
147
- const resLockNoReason = await request(app)
148
- .post('/api/team/admin/users/3/lock')
149
- .send({}) // missing reason
150
- .set('Accept', 'application/json');
151
- expect(resLockNoReason.status).toBe(400);
152
- });
153
-
154
- it('non-super-admin cannot access /admin routes even with org membership', async () => {
155
- currentUserId = 2; // Mike is admin but NOT a super-admin
156
- const res = await request(app)
157
- .get('/api/team/admin/orgs')
158
- .set('Accept', 'application/json');
159
- expect(res.status).toBe(403);
160
- });
161
-
162
- it('super-admin audit events store reason and have actor_type super_admin', async () => {
163
- // Appoint new owner with reason
164
- await request(app)
165
- .post('/api/team/admin/orgs/1/appoint-owner')
166
- .send({ target_user_id: 2, reason: 'original owner unreachable - legal request' })
167
- .set('Accept', 'application/json');
168
-
169
- const events = await pool.query(
170
- `SELECT action, actor_type, reason FROM tm_audit_events WHERE org_id = 1 ORDER BY id DESC LIMIT 1`
171
- );
172
- if (events.rows.length > 0) {
173
- const event = events.rows[0];
174
- expect(event.actor_type).toBe('super_admin');
175
- expect(event.reason).toContain('legal request');
176
- expect(event.action).toMatch(/owner|appoint/i);
177
- }
178
- });
179
-
180
- it('audit events from super-admin show actor as super_admin (not their real identity)', async () => {
181
- // Fire a super-admin action
182
- await request(app)
183
- .post('/api/team/admin/users/3/lock')
184
- .send({ reason: 'abuse report' })
185
- .set('Accept', 'application/json');
186
-
187
- // Check audit event: actor_type should be 'super_admin', actor_user_id stored internally
188
- // But the API route GET /orgs/:id/audit should show "Varshyl Support" not the real name
189
- currentUserId = 1; // owner can see audit log
190
- currentOrgId = 1;
191
- const auditRes = await request(app)
192
- .get('/api/team/orgs/1/audit')
193
- .set('Accept', 'application/json');
194
-
195
- if (auditRes.status === 200 && Array.isArray(auditRes.body)) {
196
- const superAdminEvents = auditRes.body.filter((e: Record<string, unknown>) =>
197
- e['actor_type'] === 'super_admin' || e['actor_display_name'] === 'Varshyl Support'
198
- );
199
- // If super-admin events are returned, the display should be "Varshyl Support"
200
- for (const event of superAdminEvents) {
201
- if (event['actor_display_name']) {
202
- expect(event['actor_display_name']).toBe('Varshyl Support');
203
- }
204
- // Real user_id should NOT be exposed in the public audit API
205
- expect(event).not.toHaveProperty('super_admin_user_id');
206
- }
207
- }
208
- });
209
- });
@@ -1,20 +0,0 @@
1
- /**
2
- * Vitest global setup — runs once before any test suite.
3
- * Applies all team-management migrations so integration tests have a
4
- * fully-migrated database without each test file needing to call runMigrations().
5
- */
6
- import { Pool } from 'pg';
7
-
8
- export async function setup(): Promise<void> {
9
- const databaseUrl = process.env.DATABASE_URL;
10
- if (!databaseUrl) return; // unit-only runs skip this
11
-
12
- // Dynamically import to avoid issues with import.meta.url in globalSetup context
13
- const { runMigrations } = await import('../../src/server/index.js');
14
- const pool = new Pool({ connectionString: databaseUrl });
15
- try {
16
- await runMigrations(pool);
17
- } finally {
18
- await pool.end();
19
- }
20
- }
@@ -1,330 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ROLE_HIERARCHY, roleAtLeast } from '../../src/server/types.js';
3
- import type {
4
- ServerModuleAdapter,
5
- OrgRole,
6
- TeamManagementConfig,
7
- } from '../../src/server/types.js';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Compile-time shape check: build a complete valid adapter literal.
11
- // If any required method is missing or mis-typed, tsc will error here.
12
- // ---------------------------------------------------------------------------
13
- const mockAdapter: ServerModuleAdapter = {
14
- async getCurrentUserId(_req: unknown) {
15
- return 1;
16
- },
17
- async getOrganizationIdForUser(_userId: number) {
18
- return 1;
19
- },
20
- async isUserOrgAdmin(_userId: number, _orgId: number) {
21
- return false;
22
- },
23
- logger: {
24
- info: (..._args: unknown[]) => {},
25
- warn: (..._args: unknown[]) => {},
26
- error: (..._args: unknown[]) => {},
27
- },
28
- async getUserById(_userId: number) {
29
- return { id: 1, email: 'test@example.com', name: 'Test User' };
30
- },
31
- async getUsersByIds(_userIds: number[]) {
32
- return [{ id: 1, email: 'test@example.com' }];
33
- },
34
- async findUserByEmail(_email: string) {
35
- return { id: 1, email: 'test@example.com' };
36
- },
37
- async createUserFromInvite(_params: { email: string; orgId: number; role: OrgRole }) {
38
- return { id: 2, email: 'invited@example.com' };
39
- },
40
- async setUserPassword(_userId: number, _hash: string) {},
41
- async hashPassword(_plain: string) {
42
- return 'hashed';
43
- },
44
- async verifyPassword(_plain: string, _hash: string) {
45
- return true;
46
- },
47
- async invalidateAllUserSessions(_userId: number) {},
48
- async sendInviteEmail(_params: {
49
- to: string;
50
- orgName: string;
51
- inviterName: string;
52
- role: OrgRole;
53
- magicLinkUrl: string;
54
- code: string;
55
- }) {},
56
- async sendOwnershipTransferEmail(_params: {
57
- to: string;
58
- orgName: string;
59
- fromName: string;
60
- transferUrl: string;
61
- }) {},
62
- async sendEmailChangeVerification(_params: { to: string; verifyUrl: string }) {},
63
- async sendEmailChangeOldNotice(_params: { to: string; newEmail: string; cancelUrl: string }) {},
64
- async sendEmailChangedFinalNotice(_params: {
65
- to: string;
66
- oldEmail: string;
67
- newEmail: string;
68
- }) {},
69
- async sendPasswordResetEmail(_params: { to: string; resetUrl: string }) {},
70
- async sendOrgDeletionNotice(_params: {
71
- to: string;
72
- orgName: string;
73
- scheduledFor: Date;
74
- }) {},
75
- async emitNotification(_params: { userId: number; type: string; payload: Record<string, unknown> }) {},
76
- };
77
-
78
- // Suppress unused variable warning
79
- void mockAdapter;
80
-
81
- // ---------------------------------------------------------------------------
82
- // Tests
83
- // ---------------------------------------------------------------------------
84
-
85
- describe('ServerModuleAdapter — shape (v0.1.0)', () => {
86
- it('has getCurrentUserId', () => {
87
- expect(typeof mockAdapter.getCurrentUserId).toBe('function');
88
- });
89
-
90
- it('has getOrganizationIdForUser', () => {
91
- expect(typeof mockAdapter.getOrganizationIdForUser).toBe('function');
92
- });
93
-
94
- it('has isUserOrgAdmin', () => {
95
- expect(typeof mockAdapter.isUserOrgAdmin).toBe('function');
96
- });
97
-
98
- it('has logger with info, warn, error', () => {
99
- expect(typeof mockAdapter.logger.info).toBe('function');
100
- expect(typeof mockAdapter.logger.warn).toBe('function');
101
- expect(typeof mockAdapter.logger.error).toBe('function');
102
- });
103
-
104
- it('has getUserById', () => {
105
- expect(typeof mockAdapter.getUserById).toBe('function');
106
- });
107
-
108
- it('has getUsersByIds', () => {
109
- expect(typeof mockAdapter.getUsersByIds).toBe('function');
110
- });
111
-
112
- it('has findUserByEmail', () => {
113
- expect(typeof mockAdapter.findUserByEmail).toBe('function');
114
- });
115
-
116
- it('has createUserFromInvite', () => {
117
- expect(typeof mockAdapter.createUserFromInvite).toBe('function');
118
- });
119
-
120
- it('has setUserPassword', () => {
121
- expect(typeof mockAdapter.setUserPassword).toBe('function');
122
- });
123
-
124
- it('has hashPassword', () => {
125
- expect(typeof mockAdapter.hashPassword).toBe('function');
126
- });
127
-
128
- it('has verifyPassword', () => {
129
- expect(typeof mockAdapter.verifyPassword).toBe('function');
130
- });
131
-
132
- it('has invalidateAllUserSessions', () => {
133
- expect(typeof mockAdapter.invalidateAllUserSessions).toBe('function');
134
- });
135
-
136
- it('has sendInviteEmail', () => {
137
- expect(typeof mockAdapter.sendInviteEmail).toBe('function');
138
- });
139
-
140
- it('has sendOwnershipTransferEmail', () => {
141
- expect(typeof mockAdapter.sendOwnershipTransferEmail).toBe('function');
142
- });
143
-
144
- it('has sendEmailChangeVerification', () => {
145
- expect(typeof mockAdapter.sendEmailChangeVerification).toBe('function');
146
- });
147
-
148
- it('has sendEmailChangeOldNotice', () => {
149
- expect(typeof mockAdapter.sendEmailChangeOldNotice).toBe('function');
150
- });
151
-
152
- it('has sendEmailChangedFinalNotice', () => {
153
- expect(typeof mockAdapter.sendEmailChangedFinalNotice).toBe('function');
154
- });
155
-
156
- it('has sendPasswordResetEmail', () => {
157
- expect(typeof mockAdapter.sendPasswordResetEmail).toBe('function');
158
- });
159
-
160
- it('has sendOrgDeletionNotice', () => {
161
- expect(typeof mockAdapter.sendOrgDeletionNotice).toBe('function');
162
- });
163
-
164
- it('has emitNotification', () => {
165
- expect(typeof mockAdapter.emitNotification).toBe('function');
166
- });
167
- });
168
-
169
- describe('TeamManagementConfig — feature flags', () => {
170
- it('accepts config with all flags enabled', () => {
171
- const config: TeamManagementConfig = {
172
- featureFlags: {
173
- enableInvites: true,
174
- enableAuditLog: true,
175
- enableOwnershipTransfer: true,
176
- enableEmailChange: true,
177
- enablePasswordReset: true,
178
- enableSuperAdmin: true,
179
- enableSharedAccess: true,
180
- enableHardDelete: true,
181
- },
182
- };
183
- expect(config.featureFlags?.enableInvites).toBe(true);
184
- expect(config.featureFlags?.enableAuditLog).toBe(true);
185
- expect(config.featureFlags?.enableOwnershipTransfer).toBe(true);
186
- expect(config.featureFlags?.enableEmailChange).toBe(true);
187
- expect(config.featureFlags?.enablePasswordReset).toBe(true);
188
- expect(config.featureFlags?.enableSuperAdmin).toBe(true);
189
- expect(config.featureFlags?.enableSharedAccess).toBe(true);
190
- expect(config.featureFlags?.enableHardDelete).toBe(true);
191
- });
192
-
193
- it('accepts config with all flags disabled', () => {
194
- const config: TeamManagementConfig = {
195
- featureFlags: {
196
- enableInvites: false,
197
- enableAuditLog: false,
198
- enableOwnershipTransfer: false,
199
- enableEmailChange: false,
200
- enablePasswordReset: false,
201
- enableSuperAdmin: false,
202
- enableSharedAccess: false,
203
- enableHardDelete: false,
204
- },
205
- };
206
- expect(config.featureFlags?.enableInvites).toBe(false);
207
- });
208
-
209
- it('accepts config with no featureFlags (all optional)', () => {
210
- const config: TeamManagementConfig = {
211
- };
212
- expect(config.featureFlags).toBeUndefined();
213
- });
214
-
215
- it('accepts config with partial featureFlags', () => {
216
- const config: TeamManagementConfig = {
217
- featureFlags: {
218
- enableInvites: true,
219
- },
220
- };
221
- expect(config.featureFlags?.enableInvites).toBe(true);
222
- expect(config.featureFlags?.enableAuditLog).toBeUndefined();
223
- });
224
- });
225
-
226
- describe('OrgRole — valid type values', () => {
227
- it('owner is a valid OrgRole', () => {
228
- const role: OrgRole = 'owner';
229
- expect(role).toBe('owner');
230
- });
231
-
232
- it('admin is a valid OrgRole', () => {
233
- const role: OrgRole = 'admin';
234
- expect(role).toBe('admin');
235
- });
236
-
237
- it('member is a valid OrgRole', () => {
238
- const role: OrgRole = 'member';
239
- expect(role).toBe('member');
240
- });
241
-
242
- it('viewer is a valid OrgRole', () => {
243
- const role: OrgRole = 'viewer';
244
- expect(role).toBe('viewer');
245
- });
246
- });
247
-
248
- describe('ROLE_HIERARCHY — numeric values', () => {
249
- it('owner = 40', () => {
250
- expect(ROLE_HIERARCHY['owner']).toBe(40);
251
- });
252
-
253
- it('admin = 30', () => {
254
- expect(ROLE_HIERARCHY['admin']).toBe(30);
255
- });
256
-
257
- it('member = 20', () => {
258
- expect(ROLE_HIERARCHY['member']).toBe(20);
259
- });
260
-
261
- it('viewer = 10', () => {
262
- expect(ROLE_HIERARCHY['viewer']).toBe(10);
263
- });
264
- });
265
-
266
- describe('roleAtLeast — permission matrix', () => {
267
- it('roleAtLeast("owner", "viewer") = true', () => {
268
- expect(roleAtLeast('owner', 'viewer')).toBe(true);
269
- });
270
-
271
- it('roleAtLeast("viewer", "admin") = false', () => {
272
- expect(roleAtLeast('viewer', 'admin')).toBe(false);
273
- });
274
-
275
- it('roleAtLeast("admin", "admin") = true (equal roles)', () => {
276
- expect(roleAtLeast('admin', 'admin')).toBe(true);
277
- });
278
-
279
- it('roleAtLeast("owner", "owner") = true', () => {
280
- expect(roleAtLeast('owner', 'owner')).toBe(true);
281
- });
282
-
283
- it('roleAtLeast("owner", "admin") = true', () => {
284
- expect(roleAtLeast('owner', 'admin')).toBe(true);
285
- });
286
-
287
- it('roleAtLeast("owner", "member") = true', () => {
288
- expect(roleAtLeast('owner', 'member')).toBe(true);
289
- });
290
-
291
- it('roleAtLeast("admin", "member") = true', () => {
292
- expect(roleAtLeast('admin', 'member')).toBe(true);
293
- });
294
-
295
- it('roleAtLeast("admin", "viewer") = true', () => {
296
- expect(roleAtLeast('admin', 'viewer')).toBe(true);
297
- });
298
-
299
- it('roleAtLeast("member", "member") = true', () => {
300
- expect(roleAtLeast('member', 'member')).toBe(true);
301
- });
302
-
303
- it('roleAtLeast("member", "viewer") = true', () => {
304
- expect(roleAtLeast('member', 'viewer')).toBe(true);
305
- });
306
-
307
- it('roleAtLeast("viewer", "viewer") = true', () => {
308
- expect(roleAtLeast('viewer', 'viewer')).toBe(true);
309
- });
310
-
311
- it('roleAtLeast("viewer", "member") = false', () => {
312
- expect(roleAtLeast('viewer', 'member')).toBe(false);
313
- });
314
-
315
- it('roleAtLeast("viewer", "owner") = false', () => {
316
- expect(roleAtLeast('viewer', 'owner')).toBe(false);
317
- });
318
-
319
- it('roleAtLeast("member", "admin") = false', () => {
320
- expect(roleAtLeast('member', 'admin')).toBe(false);
321
- });
322
-
323
- it('roleAtLeast("member", "owner") = false', () => {
324
- expect(roleAtLeast('member', 'owner')).toBe(false);
325
- });
326
-
327
- it('roleAtLeast("admin", "owner") = false', () => {
328
- expect(roleAtLeast('admin', 'owner')).toBe(false);
329
- });
330
- });