@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,143 +0,0 @@
1
- import { Router } from 'express';
2
- import type { Pool } from 'pg';
3
- import type { ServerModuleAdapter, TeamManagementFeatureFlags } from '../types.js';
4
- import { requestEmailChange, verifyEmailChange, cancelEmailChange } from '../services/email-change.service.js';
5
- import { requestPasswordReset, resetPassword } from '../services/password-reset.service.js';
6
- import { getActiveMembership } from '../services/organizations.service.js';
7
- import { sha256 } from '../crypto.js';
8
-
9
- export function createMeRouter(
10
- pool: Pool,
11
- adapter: ServerModuleAdapter,
12
- flags: TeamManagementFeatureFlags,
13
- baseUrl: string
14
- ): Router {
15
- const router = Router();
16
-
17
- router.get('/membership', async (req, res) => {
18
- try {
19
- const userId = await adapter.getCurrentUserId(req);
20
- if (!userId) { res.status(401).json({ error: 'Authentication required' }); return; }
21
- const orgId = await adapter.getOrganizationIdForUser(userId);
22
- if (!orgId) { res.status(404).json({ error: 'No organization membership found' }); return; }
23
- const membership = await getActiveMembership(pool, orgId, userId);
24
- res.json({ membership });
25
- } catch (e) {
26
- adapter.logger.error('[me] GET /membership', { error: (e as Error).message });
27
- res.status(500).json({ error: 'Failed to fetch membership' });
28
- }
29
- });
30
-
31
- router.post('/email-change', async (req, res) => {
32
- if (!flags.enableEmailChange) { res.status(501).json({ error: 'Email change is not enabled' }); return; }
33
- try {
34
- const userId = await adapter.getCurrentUserId(req);
35
- if (!userId) { res.status(401).json({ error: 'Authentication required' }); return; }
36
- const user = await adapter.getUserById(userId);
37
- if (!user) { res.status(404).json({ error: 'User not found' }); return; }
38
- const { newEmail } = req.body as { newEmail?: string };
39
- if (!newEmail) { res.status(400).json({ error: 'newEmail is required' }); return; }
40
- await requestEmailChange(pool, adapter, { userId, currentEmail: user.email, newEmail, baseUrl });
41
- res.json({ message: 'Verification email sent to your new address' });
42
- } catch (e) {
43
- const msg = (e as Error).message;
44
- adapter.logger.error('[me] POST /email-change', { error: msg });
45
- if (msg.includes('Too many')) {
46
- res.status(429).json({ error: msg });
47
- } else if (msg.includes('already in use')) {
48
- res.status(422).json({ error: msg });
49
- } else {
50
- res.status(500).json({ error: 'Failed to request email change' });
51
- }
52
- }
53
- });
54
-
55
- router.get('/email-change/verify', async (req, res) => {
56
- if (!flags.enableEmailChange) { res.status(501).json({ error: 'Email change is not enabled' }); return; }
57
- const token = req.query.token as string;
58
- if (!token) { res.status(400).json({ error: 'token query parameter is required' }); return; }
59
- try {
60
- // Token-based verification — no authentication required; token is self-authenticating
61
- const userId = await adapter.getCurrentUserId(req);
62
- await verifyEmailChange(pool, adapter, { token, userId: userId ?? null });
63
- res.json({ message: 'Email address updated successfully' });
64
- } catch (e) {
65
- const msg = (e as Error).message;
66
- adapter.logger.error('[me] GET /email-change/verify', { error: msg });
67
- if (msg.includes('Invalid') || msg.includes('expired')) {
68
- res.status(404).json({ error: msg });
69
- } else {
70
- res.status(500).json({ error: 'Failed to verify email change' });
71
- }
72
- }
73
- });
74
-
75
- router.get('/email-change/cancel', async (req, res) => {
76
- if (!flags.enableEmailChange) { res.status(501).json({ error: 'Email change is not enabled' }); return; }
77
- const token = req.query.token as string;
78
- if (!token) { res.status(400).json({ error: 'token query parameter is required' }); return; }
79
- try {
80
- await cancelEmailChange(pool, adapter, { token });
81
- res.json({ message: 'Email change cancelled. Your sessions have been invalidated for security.' });
82
- } catch (e) {
83
- const msg = (e as Error).message;
84
- adapter.logger.error('[me] GET /email-change/cancel', { error: msg });
85
- if (msg.includes('Invalid') || msg.includes('expired')) {
86
- res.status(404).json({ error: msg });
87
- } else {
88
- res.status(500).json({ error: 'Failed to cancel email change' });
89
- }
90
- }
91
- });
92
-
93
- router.post('/password-reset/request', async (req, res) => {
94
- if (!flags.enablePasswordReset) { res.status(501).json({ error: 'Password reset is not enabled' }); return; }
95
- const { email } = req.body as { email?: string };
96
- if (!email) { res.status(400).json({ error: 'email is required' }); return; }
97
- try {
98
- await requestPasswordReset(pool, adapter, { email, baseUrl });
99
- res.json({ message: 'If that email exists, a reset link has been sent' });
100
- } catch (e) {
101
- adapter.logger.error('[me] POST /password-reset/request', { error: (e as Error).message });
102
- res.json({ message: 'If that email exists, a reset link has been sent' });
103
- }
104
- });
105
-
106
- router.get('/password-reset', async (req, res) => {
107
- if (!flags.enablePasswordReset) { res.status(501).json({ error: 'Password reset is not enabled' }); return; }
108
- const token = req.query.token as string;
109
- if (!token) { res.status(400).json({ error: 'token query parameter is required' }); return; }
110
- try {
111
- const tokenHash = sha256(token);
112
- const result = await pool.query(
113
- `SELECT id FROM tm_password_reset_requests WHERE token_hash = $1 AND used_at IS NULL AND expires_at > NOW()`,
114
- [tokenHash]
115
- );
116
- if (result.rows.length === 0) { res.status(404).json({ error: 'Invalid or expired password reset token' }); return; }
117
- res.json({ valid: true });
118
- } catch (e) {
119
- adapter.logger.error('[me] GET /password-reset', { error: (e as Error).message });
120
- res.status(500).json({ error: 'Failed to validate token' });
121
- }
122
- });
123
-
124
- router.post('/password-reset', async (req, res) => {
125
- if (!flags.enablePasswordReset) { res.status(501).json({ error: 'Password reset is not enabled' }); return; }
126
- const { token, newPassword } = req.body as { token?: string; newPassword?: string };
127
- if (!token || !newPassword) { res.status(400).json({ error: 'token and newPassword are required' }); return; }
128
- try {
129
- await resetPassword(pool, adapter, { token, newPassword });
130
- res.json({ message: 'Password updated successfully. Please log in again.' });
131
- } catch (e) {
132
- const msg = (e as Error).message;
133
- adapter.logger.error('[me] POST /password-reset', { error: msg });
134
- if (msg.includes('Invalid') || msg.includes('expired') || msg.includes('8 characters')) {
135
- res.status(422).json({ error: msg });
136
- } else {
137
- res.status(500).json({ error: 'Failed to reset password' });
138
- }
139
- }
140
- });
141
-
142
- return router;
143
- }
@@ -1,428 +0,0 @@
1
- import { Router } from 'express';
2
- import type { Pool } from 'pg';
3
- import type { ServerModuleAdapter, TeamManagementFeatureFlags, OrgRole } from '../types.js';
4
- import { requireMembership, type AuthenticatedRequest } from '../middleware/require-membership.js';
5
- import { requireRole } from '../middleware/require-role.js';
6
- import { getOrg, updateOrg, softDeleteOrg, listOrgMembers } from '../services/organizations.service.js';
7
- import { removeMember, changeRole, validateRoleChange } from '../services/memberships.service.js';
8
- import { getPendingTransfer } from '../services/ownership.service.js';
9
- import { writeAuditEvent, getClientIp } from '../services/audit.service.js';
10
-
11
- export function createOrgsRouter(
12
- pool: Pool,
13
- adapter: ServerModuleAdapter,
14
- flags: TeamManagementFeatureFlags
15
- ): Router {
16
- const router = Router({ mergeParams: true });
17
- const authMiddleware = requireMembership(pool, adapter);
18
-
19
- // POST /orgs — create org (any authenticated user)
20
- router.post('/', async (req, res) => {
21
- try {
22
- const userId = await adapter.getCurrentUserId(req as import('express').Request);
23
- if (!userId) {
24
- res.status(401).json({ error: 'Authentication required' });
25
- return;
26
- }
27
- const { name, slug, settings } = req.body as { name?: string; slug?: string; settings?: Record<string, unknown> };
28
- if (!name || !slug) {
29
- res.status(400).json({ error: 'name and slug are required' });
30
- return;
31
- }
32
-
33
- const result = await pool.query(
34
- `INSERT INTO tm_organizations (name, slug, owner_user_id, settings)
35
- VALUES ($1, $2, $3, $4) RETURNING *`,
36
- [name, slug, userId, JSON.stringify(settings ?? {})]
37
- );
38
- const org = result.rows[0];
39
-
40
- await pool.query(
41
- `INSERT INTO tm_memberships (org_id, user_id, role, joined_at)
42
- VALUES ($1, $2, 'owner', NOW())`,
43
- [org.id, userId]
44
- );
45
-
46
- if (flags.enableAuditLog) {
47
- await writeAuditEvent({
48
- pool,
49
- orgId: org.id,
50
- actorUserId: userId,
51
- action: 'org.created',
52
- targetType: 'org',
53
- targetId: org.id,
54
- after: { name, slug },
55
- ip: getClientIp(req),
56
- userAgent: req.headers['user-agent'] ?? null,
57
- });
58
- }
59
-
60
- res.status(201).json({ org });
61
- } catch (e) {
62
- const msg = (e as Error).message;
63
- adapter.logger.error('[orgs] POST /', { error: msg });
64
- if (msg.includes('unique') || msg.includes('duplicate') || msg.includes('already exists')) {
65
- res.status(409).json({ error: 'Organization with that slug already exists' });
66
- } else {
67
- res.status(500).json({ error: 'Failed to create organization' });
68
- }
69
- }
70
- });
71
-
72
- // GET /orgs/:orgId — org info (member+)
73
- router.get('/:orgId', authMiddleware, async (req, res) => {
74
- const { orgId } = req as AuthenticatedRequest;
75
- try {
76
- const org = await getOrg(pool, orgId);
77
- if (!org) {
78
- res.status(404).json({ error: 'Organization not found' });
79
- return;
80
- }
81
- res.json({ org });
82
- } catch (e) {
83
- adapter.logger.error('[orgs] GET /:orgId', { error: (e as Error).message });
84
- res.status(500).json({ error: 'Failed to fetch organization' });
85
- }
86
- });
87
-
88
- // PATCH /orgs/:orgId — update name/slug/settings (admin+)
89
- router.patch('/:orgId', authMiddleware, requireRole('admin'), async (req, res) => {
90
- const { orgId, userId } = req as AuthenticatedRequest;
91
- const { name, slug } = req.body as { name?: string; slug?: string };
92
- try {
93
- const before = await getOrg(pool, orgId);
94
- const updated = await updateOrg(pool, orgId, { name, slug });
95
-
96
- if (flags.enableAuditLog) {
97
- await writeAuditEvent({
98
- pool,
99
- orgId,
100
- actorUserId: userId,
101
- action: 'org.settings.updated',
102
- targetType: 'org',
103
- targetId: orgId,
104
- before: { name: before?.name, slug: before?.slug },
105
- after: { name: updated.name, slug: updated.slug },
106
- ip: getClientIp(req),
107
- userAgent: req.headers['user-agent'] ?? null,
108
- });
109
- }
110
-
111
- res.json({ org: updated });
112
- } catch (e) {
113
- adapter.logger.error('[orgs] PATCH /:orgId', { error: (e as Error).message });
114
- res.status(500).json({ error: 'Failed to update organization' });
115
- }
116
- });
117
-
118
- // DELETE /orgs/:orgId — soft delete (owner only), requires confirmName
119
- router.delete('/:orgId', authMiddleware, requireRole('owner'), async (req, res) => {
120
- const { orgId, userId } = req as AuthenticatedRequest;
121
- // Accept both confirmName and confirmOrgName for compatibility
122
- const { confirmName, confirmOrgName } = req.body as { confirmName?: string; confirmOrgName?: string };
123
- const confirm = confirmName ?? confirmOrgName;
124
- try {
125
- const org = await getOrg(pool, orgId);
126
- if (!org) {
127
- res.status(404).json({ error: 'Organization not found' });
128
- return;
129
- }
130
- if (!confirm || confirm !== org.name) {
131
- res.status(422).json({ error: 'Confirmation name does not match organization name' });
132
- return;
133
- }
134
-
135
- // Transfer lock: cannot delete org while ownership transfer is pending
136
- const pendingTransfer = await getPendingTransfer(pool, orgId);
137
- if (pendingTransfer) {
138
- res.status(409).json({ error: 'Cannot delete organization while an ownership transfer is pending. Cancel the transfer first.' });
139
- return;
140
- }
141
-
142
- await softDeleteOrg(pool, orgId, userId);
143
-
144
- if (flags.enableAuditLog) {
145
- await writeAuditEvent({
146
- pool,
147
- orgId,
148
- actorUserId: userId,
149
- action: 'org.deleted',
150
- targetType: 'org',
151
- targetId: orgId,
152
- ip: getClientIp(req),
153
- userAgent: req.headers['user-agent'] ?? null,
154
- });
155
- }
156
-
157
- try {
158
- const members = await listOrgMembers(pool, orgId, { includeRemoved: false });
159
- const userIds = members.map(m => m.user_id);
160
- const users = await adapter.getUsersByIds(userIds);
161
- const scheduledFor = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
162
- for (const user of users) {
163
- await adapter.sendOrgDeletionNotice({
164
- to: user.email,
165
- orgName: org.name,
166
- scheduledFor,
167
- });
168
- }
169
- } catch (e) {
170
- adapter.logger.warn('[orgs] Failed to send deletion notices', { error: (e as Error).message });
171
- }
172
-
173
- res.json({ message: 'Organization scheduled for deletion in 30 days' });
174
- } catch (e) {
175
- adapter.logger.error('[orgs] DELETE /:orgId', { error: (e as Error).message });
176
- res.status(500).json({ error: 'Failed to delete organization' });
177
- }
178
- });
179
-
180
- // GET /orgs/:orgId/members — list members (member+)
181
- router.get('/:orgId/members', authMiddleware, async (req, res) => {
182
- const { orgId } = req as AuthenticatedRequest;
183
- try {
184
- const members = await listOrgMembers(pool, orgId);
185
- const userIds = members.map(m => m.user_id);
186
- const users = await adapter.getUsersByIds(userIds);
187
- const userMap = new Map(users.map(u => [u.id, u]));
188
- const enriched = members.map(m => ({ ...m, user: userMap.get(m.user_id) }));
189
- res.json({ members: enriched });
190
- } catch (e) {
191
- adapter.logger.error('[orgs] GET /:orgId/members', { error: (e as Error).message });
192
- res.status(500).json({ error: 'Failed to fetch members' });
193
- }
194
- });
195
-
196
- // GET /orgs/:orgId/members/former — former members (admin+)
197
- router.get('/:orgId/members/former', authMiddleware, requireRole('admin'), async (req, res) => {
198
- const { orgId } = req as AuthenticatedRequest;
199
- try {
200
- const allMembers = await listOrgMembers(pool, orgId, { includeRemoved: true });
201
- const former = allMembers.filter(m => m.removed_at !== null);
202
- res.json({ members: former });
203
- } catch (e) {
204
- adapter.logger.error('[orgs] GET /:orgId/members/former', { error: (e as Error).message });
205
- res.status(500).json({ error: 'Failed to fetch former members' });
206
- }
207
- });
208
-
209
- // DELETE /orgs/:orgId/members/:userId — remove member (admin+)
210
- router.delete('/:orgId/members/:userId', authMiddleware, requireRole('admin'), async (req, res) => {
211
- const { orgId, userId: actorId, userRole } = req as AuthenticatedRequest;
212
- const targetUserId = parseInt(req.params.userId, 10);
213
- const { reason } = req.body as { reason?: string };
214
-
215
- if (isNaN(targetUserId)) {
216
- res.status(400).json({ error: 'Invalid user ID' });
217
- return;
218
- }
219
- if (targetUserId === actorId) {
220
- res.status(400).json({ message: 'Cannot remove yourself: you are the owner of this organization' });
221
- return;
222
- }
223
-
224
- try {
225
- const targetMemberResult = await pool.query(
226
- `SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`,
227
- [orgId, targetUserId]
228
- );
229
- if (targetMemberResult.rows.length === 0) {
230
- res.status(404).json({ error: 'Member not found' });
231
- return;
232
- }
233
- const targetRole = targetMemberResult.rows[0].role;
234
-
235
- if (userRole === 'admin' && (targetRole === 'owner' || targetRole === 'admin')) {
236
- res.status(403).json({ error: 'Admins cannot remove owners or other admins' });
237
- return;
238
- }
239
-
240
- // Transfer lock: cannot remove a user involved in a pending transfer
241
- const pendingTransferForRemove = await getPendingTransfer(pool, orgId);
242
- if (pendingTransferForRemove &&
243
- (pendingTransferForRemove.from_user_id === targetUserId ||
244
- pendingTransferForRemove.to_user_id === targetUserId)) {
245
- res.status(409).json({ error: 'Cannot remove a member involved in a pending ownership transfer. Cancel the transfer first.' });
246
- return;
247
- }
248
-
249
- await removeMember(pool, { orgId, userId: targetUserId, removedByUserId: actorId, reason });
250
-
251
- if (flags.enableAuditLog) {
252
- await writeAuditEvent({
253
- pool,
254
- orgId,
255
- actorUserId: actorId,
256
- action: 'member.removed',
257
- targetType: 'user',
258
- targetId: targetUserId,
259
- before: { role: targetRole },
260
- reason: reason ?? null,
261
- ip: getClientIp(req),
262
- userAgent: req.headers['user-agent'] ?? null,
263
- });
264
- }
265
-
266
- res.json({ message: 'Member removed successfully' });
267
- } catch (e) {
268
- adapter.logger.error('[orgs] DELETE /:orgId/members/:userId', { error: (e as Error).message });
269
- res.status(500).json({ error: 'Failed to remove member' });
270
- }
271
- });
272
-
273
- // PATCH /orgs/:orgId/members/:userId/role — change role (admin+)
274
- router.patch('/:orgId/members/:userId/role', authMiddleware, requireRole('admin'), async (req, res) => {
275
- const { orgId, userId: actorId, userRole } = req as AuthenticatedRequest;
276
- const targetUserId = parseInt(req.params.userId, 10);
277
- const { role: newRole } = req.body as { role?: string };
278
-
279
- if (isNaN(targetUserId)) {
280
- res.status(400).json({ error: 'Invalid user ID' });
281
- return;
282
- }
283
- if (!newRole) {
284
- res.status(400).json({ error: 'role is required' });
285
- return;
286
- }
287
-
288
- try {
289
- await validateRoleChange(pool, { orgId, actorRole: userRole, targetUserId, newRole: newRole as OrgRole });
290
-
291
- // Transfer lock: cannot change role of a user involved in a pending transfer
292
- const pendingTransferForPatch = await getPendingTransfer(pool, orgId);
293
- if (pendingTransferForPatch &&
294
- (pendingTransferForPatch.from_user_id === targetUserId ||
295
- pendingTransferForPatch.to_user_id === targetUserId)) {
296
- res.status(409).json({ error: 'Cannot change role of a member involved in a pending ownership transfer. Cancel the transfer first.' });
297
- return;
298
- }
299
-
300
- const before = await pool.query(
301
- `SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`,
302
- [orgId, targetUserId]
303
- );
304
- const updated = await changeRole(pool, { orgId, userId: targetUserId, newRole: newRole as OrgRole, changedByUserId: actorId });
305
-
306
- if (flags.enableAuditLog) {
307
- await writeAuditEvent({
308
- pool,
309
- orgId,
310
- actorUserId: actorId,
311
- action: 'member.role_changed',
312
- targetType: 'user',
313
- targetId: targetUserId,
314
- before: { role: before.rows[0]?.role },
315
- after: { role: newRole },
316
- ip: getClientIp(req),
317
- userAgent: req.headers['user-agent'] ?? null,
318
- });
319
- }
320
-
321
- res.json({ membership: updated });
322
- } catch (e) {
323
- const msg = (e as Error).message;
324
- adapter.logger.error('[orgs] PATCH /:orgId/members/:userId/role', { error: msg });
325
- if (msg.includes('Cannot') || msg.includes('Requires') || msg.includes('cannot')) {
326
- res.status(403).json({ error: msg });
327
- } else {
328
- res.status(500).json({ error: 'Failed to change role' });
329
- }
330
- }
331
- });
332
-
333
- // PATCH /orgs/:orgId/members/:userId — alias without /role suffix (for compatibility)
334
- router.patch('/:orgId/members/:userId', authMiddleware, requireRole('admin'), async (req, res) => {
335
- const { orgId, userId: actorId, userRole } = req as AuthenticatedRequest;
336
- const targetUserId = parseInt(req.params.userId, 10);
337
- const { role: newRole } = req.body as { role?: string };
338
-
339
- if (isNaN(targetUserId)) {
340
- res.status(400).json({ error: 'Invalid user ID' });
341
- return;
342
- }
343
- if (!newRole) {
344
- res.status(400).json({ error: 'role is required' });
345
- return;
346
- }
347
-
348
- try {
349
- await validateRoleChange(pool, { orgId, actorRole: userRole, targetUserId, newRole: newRole as OrgRole });
350
-
351
- // Transfer lock: cannot change role of a user involved in a pending transfer
352
- const pendingTransferForPatch = await getPendingTransfer(pool, orgId);
353
- if (pendingTransferForPatch &&
354
- (pendingTransferForPatch.from_user_id === targetUserId ||
355
- pendingTransferForPatch.to_user_id === targetUserId)) {
356
- res.status(409).json({ error: 'Cannot change role of a member involved in a pending ownership transfer. Cancel the transfer first.' });
357
- return;
358
- }
359
-
360
- const before = await pool.query(
361
- `SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`,
362
- [orgId, targetUserId]
363
- );
364
- const updated = await changeRole(pool, { orgId, userId: targetUserId, newRole: newRole as OrgRole, changedByUserId: actorId });
365
-
366
- if (flags.enableAuditLog) {
367
- await writeAuditEvent({
368
- pool,
369
- orgId,
370
- actorUserId: actorId,
371
- action: 'member.role_changed',
372
- targetType: 'user',
373
- targetId: targetUserId,
374
- before: { role: before.rows[0]?.role },
375
- after: { role: newRole },
376
- ip: getClientIp(req),
377
- userAgent: req.headers['user-agent'] ?? null,
378
- });
379
- }
380
-
381
- res.json({ membership: updated });
382
- } catch (e) {
383
- const msg = (e as Error).message;
384
- adapter.logger.error('[orgs] PATCH /:orgId/members/:userId', { error: msg });
385
- if (msg.includes('Cannot') || msg.includes('Requires') || msg.includes('cannot')) {
386
- res.status(403).json({ error: msg });
387
- } else {
388
- res.status(500).json({ error: 'Failed to change role' });
389
- }
390
- }
391
- });
392
-
393
-
394
- // GET /orgs/:orgId/members/:userId/cascade-preview — preview cascade effects of removing a member (admin+)
395
- router.get('/:orgId/members/:userId/cascade-preview', authMiddleware, requireRole('admin'), async (req, res) => {
396
- const { orgId } = req as AuthenticatedRequest;
397
- const targetUserId = parseInt(req.params.userId, 10);
398
- if (isNaN(targetUserId)) {
399
- res.status(400).json({ error: 'Invalid user ID' });
400
- return;
401
- }
402
- try {
403
- const membershipResult = await pool.query(
404
- `SELECT * FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`,
405
- [orgId, targetUserId]
406
- );
407
- if (membershipResult.rows.length === 0) {
408
- res.status(404).json({ error: 'Member not found' });
409
- return;
410
- }
411
- const membership = membershipResult.rows[0];
412
-
413
- const invitationsResult = await pool.query(
414
- `SELECT * FROM tm_invitations
415
- WHERE org_id = $1 AND invited_by_user_id = $2
416
- AND revoked_at IS NULL AND accepted_at IS NULL AND expires_at > NOW()`,
417
- [orgId, targetUserId]
418
- );
419
-
420
- res.json({ membership, pendingInvitations: invitationsResult.rows });
421
- } catch (e) {
422
- adapter.logger.error('[orgs] GET cascade-preview', { error: (e as Error).message });
423
- res.status(500).json({ error: 'Failed to fetch cascade preview' });
424
- }
425
- });
426
-
427
- return router;
428
- }
@@ -1,110 +0,0 @@
1
- import { Router } from 'express';
2
- import type { Pool } from 'pg';
3
- import type { ServerModuleAdapter, TeamManagementFeatureFlags } from '../types.js';
4
- import { requireMembership, type AuthenticatedRequest } from '../middleware/require-membership.js';
5
- import { requireRole } from '../middleware/require-role.js';
6
- import {
7
- initiateTransfer,
8
- acceptTransfer,
9
- cancelTransfer,
10
- getPendingTransfer,
11
- } from '../services/ownership.service.js';
12
- import { writeAuditEvent, getClientIp } from '../services/audit.service.js';
13
-
14
- export function createTransferRouter(
15
- pool: Pool,
16
- adapter: ServerModuleAdapter,
17
- flags: TeamManagementFeatureFlags,
18
- baseUrl: string
19
- ): Router {
20
- const router = Router({ mergeParams: true });
21
- const authMiddleware = requireMembership(pool, adapter);
22
-
23
- function featureCheck(res: import('express').Response): boolean {
24
- if (!flags.enableOwnershipTransfer) {
25
- res.status(501).json({ error: 'Ownership transfer is not enabled' });
26
- return false;
27
- }
28
- return true;
29
- }
30
-
31
- router.get('/:orgId/transfer', authMiddleware, async (req, res) => {
32
- if (!featureCheck(res)) return;
33
- const { orgId } = req as AuthenticatedRequest;
34
- try {
35
- const transfer = await getPendingTransfer(pool, orgId);
36
- res.json({ transfer });
37
- } catch (e) {
38
- adapter.logger.error('[transfer] GET', { error: (e as Error).message });
39
- res.status(500).json({ error: 'Failed to fetch transfer' });
40
- }
41
- });
42
-
43
- router.post('/:orgId/transfer', authMiddleware, requireRole('owner'), async (req, res) => {
44
- if (!featureCheck(res)) return;
45
- const { orgId, userId } = req as AuthenticatedRequest;
46
- const { toUserId } = req.body as { toUserId?: number };
47
- if (!toUserId) { res.status(400).json({ error: 'toUserId is required' }); return; }
48
- try {
49
- const transfer = await initiateTransfer(pool, adapter, { orgId, fromUserId: userId, toUserId, baseUrl });
50
- if (flags.enableAuditLog) {
51
- await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_initiated',
52
- targetType: 'user', targetId: toUserId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
53
- }
54
- res.status(201).json({ transfer });
55
- } catch (e) {
56
- const msg = (e as Error).message;
57
- adapter.logger.error('[transfer] POST initiate', { error: msg });
58
- if (msg.includes('not a member') || msg.includes('admin') || msg.includes('pending')) {
59
- res.status(422).json({ error: msg });
60
- } else {
61
- res.status(500).json({ error: 'Failed to initiate transfer' });
62
- }
63
- }
64
- });
65
-
66
- router.post('/:orgId/transfer/accept', authMiddleware, async (req, res) => {
67
- if (!featureCheck(res)) return;
68
- const { orgId, userId } = req as AuthenticatedRequest;
69
- try {
70
- await acceptTransfer(pool, adapter, { orgId, acceptingUserId: userId });
71
- if (flags.enableAuditLog) {
72
- await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_accepted',
73
- targetType: 'org', targetId: orgId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
74
- }
75
- res.json({ message: 'Ownership transfer accepted. You are now the owner.' });
76
- } catch (e) {
77
- const msg = (e as Error).message;
78
- adapter.logger.error('[transfer] POST accept', { error: msg });
79
- if (msg.includes('No valid') || msg.includes('Only the designated')) {
80
- res.status(422).json({ error: msg });
81
- } else {
82
- res.status(500).json({ error: 'Failed to accept transfer' });
83
- }
84
- }
85
- });
86
-
87
- router.delete('/:orgId/transfer', authMiddleware, async (req, res) => {
88
- if (!featureCheck(res)) return;
89
- const { orgId, userId, userRole } = req as AuthenticatedRequest;
90
- try {
91
- const pending = await getPendingTransfer(pool, orgId);
92
- if (!pending) { res.status(404).json({ error: 'No pending transfer found' }); return; }
93
- if (userRole !== 'owner' && pending.to_user_id !== userId) {
94
- res.status(403).json({ error: 'Only the initiating owner or designated recipient can cancel this transfer' });
95
- return;
96
- }
97
- await cancelTransfer(pool, { orgId, cancelledByUserId: userId });
98
- if (flags.enableAuditLog) {
99
- await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_cancelled',
100
- targetType: 'org', targetId: orgId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
101
- }
102
- res.json({ message: 'Transfer cancelled' });
103
- } catch (e) {
104
- adapter.logger.error('[transfer] DELETE cancel', { error: (e as Error).message });
105
- res.status(500).json({ error: 'Failed to cancel transfer' });
106
- }
107
- });
108
-
109
- return router;
110
- }
File without changes