@varshylinc/team-management 0.1.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 (203) hide show
  1. package/.eslintrc.cjs +18 -0
  2. package/CHANGELOG.md +159 -0
  3. package/LICENSE +6 -0
  4. package/README.md +97 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/server/crypto.d.ts +6 -0
  10. package/dist/server/crypto.d.ts.map +1 -0
  11. package/dist/server/crypto.js +42 -0
  12. package/dist/server/crypto.js.map +1 -0
  13. package/dist/server/index.d.ts +34 -0
  14. package/dist/server/index.d.ts.map +1 -0
  15. package/dist/server/index.js +114 -0
  16. package/dist/server/index.js.map +1 -0
  17. package/dist/server/middleware/require-membership.d.ts +10 -0
  18. package/dist/server/middleware/require-membership.d.ts.map +1 -0
  19. package/dist/server/middleware/require-membership.js +33 -0
  20. package/dist/server/middleware/require-membership.js.map +1 -0
  21. package/dist/server/middleware/require-role.d.ts +4 -0
  22. package/dist/server/middleware/require-role.d.ts.map +1 -0
  23. package/dist/server/middleware/require-role.js +16 -0
  24. package/dist/server/middleware/require-role.js.map +1 -0
  25. package/dist/server/middleware/require-super-admin.d.ts +5 -0
  26. package/dist/server/middleware/require-super-admin.d.ts.map +1 -0
  27. package/dist/server/middleware/require-super-admin.js +27 -0
  28. package/dist/server/middleware/require-super-admin.js.map +1 -0
  29. package/dist/server/migrations/0001_create_tm_schema_migrations.sql +13 -0
  30. package/dist/server/migrations/0002_create_tm_organizations.sql +14 -0
  31. package/dist/server/migrations/0003_create_tm_memberships.sql +24 -0
  32. package/dist/server/migrations/0004_create_tm_invitations.sql +22 -0
  33. package/dist/server/migrations/0005_create_tm_audit_events.sql +17 -0
  34. package/dist/server/migrations/0006_create_tm_email_change_requests.sql +13 -0
  35. package/dist/server/migrations/0007_create_tm_ownership_transfers.sql +22 -0
  36. package/dist/server/migrations/0008_create_tm_super_admins.sql +8 -0
  37. package/dist/server/migrations/0009_create_tm_password_reset_requests.sql +9 -0
  38. package/dist/server/migrations/0010_create_tm_shared_access.sql +8 -0
  39. package/dist/server/migrations/0011_seed_super_admin.sql +15 -0
  40. package/dist/server/migrations/0012_create_tm_user_locks.sql +7 -0
  41. package/dist/server/routes/admin.routes.d.ts +5 -0
  42. package/dist/server/routes/admin.routes.d.ts.map +1 -0
  43. package/dist/server/routes/admin.routes.js +262 -0
  44. package/dist/server/routes/admin.routes.js.map +1 -0
  45. package/dist/server/routes/audit.routes.d.ts +5 -0
  46. package/dist/server/routes/audit.routes.d.ts.map +1 -0
  47. package/dist/server/routes/audit.routes.js +70 -0
  48. package/dist/server/routes/audit.routes.js.map +1 -0
  49. package/dist/server/routes/health.routes.d.ts +8 -0
  50. package/dist/server/routes/health.routes.d.ts.map +1 -0
  51. package/dist/server/routes/health.routes.js +39 -0
  52. package/dist/server/routes/health.routes.js.map +1 -0
  53. package/dist/server/routes/invitations.routes.d.ts +5 -0
  54. package/dist/server/routes/invitations.routes.d.ts.map +1 -0
  55. package/dist/server/routes/invitations.routes.js +232 -0
  56. package/dist/server/routes/invitations.routes.js.map +1 -0
  57. package/dist/server/routes/me.routes.d.ts +5 -0
  58. package/dist/server/routes/me.routes.d.ts.map +1 -0
  59. package/dist/server/routes/me.routes.js +188 -0
  60. package/dist/server/routes/me.routes.js.map +1 -0
  61. package/dist/server/routes/orgs.routes.d.ts +5 -0
  62. package/dist/server/routes/orgs.routes.d.ts.map +1 -0
  63. package/dist/server/routes/orgs.routes.js +371 -0
  64. package/dist/server/routes/orgs.routes.js.map +1 -0
  65. package/dist/server/routes/transfer.routes.d.ts +5 -0
  66. package/dist/server/routes/transfer.routes.d.ts.map +1 -0
  67. package/dist/server/routes/transfer.routes.js +108 -0
  68. package/dist/server/routes/transfer.routes.js.map +1 -0
  69. package/dist/server/services/audit.service.d.ts +20 -0
  70. package/dist/server/services/audit.service.d.ts.map +1 -0
  71. package/dist/server/services/audit.service.js +23 -0
  72. package/dist/server/services/audit.service.js.map +1 -0
  73. package/dist/server/services/email-change.service.d.ts +16 -0
  74. package/dist/server/services/email-change.service.d.ts.map +1 -0
  75. package/dist/server/services/email-change.service.js +107 -0
  76. package/dist/server/services/email-change.service.js.map +1 -0
  77. package/dist/server/services/invitations.service.d.ts +41 -0
  78. package/dist/server/services/invitations.service.d.ts.map +1 -0
  79. package/dist/server/services/invitations.service.js +214 -0
  80. package/dist/server/services/invitations.service.js.map +1 -0
  81. package/dist/server/services/memberships.service.d.ts +27 -0
  82. package/dist/server/services/memberships.service.d.ts.map +1 -0
  83. package/dist/server/services/memberships.service.js +69 -0
  84. package/dist/server/services/memberships.service.js.map +1 -0
  85. package/dist/server/services/organizations.service.d.ts +19 -0
  86. package/dist/server/services/organizations.service.d.ts.map +1 -0
  87. package/dist/server/services/organizations.service.js +61 -0
  88. package/dist/server/services/organizations.service.js.map +1 -0
  89. package/dist/server/services/ownership.service.d.ts +19 -0
  90. package/dist/server/services/ownership.service.d.ts.map +1 -0
  91. package/dist/server/services/ownership.service.js +102 -0
  92. package/dist/server/services/ownership.service.js.map +1 -0
  93. package/dist/server/services/password-reset.service.d.ts +12 -0
  94. package/dist/server/services/password-reset.service.d.ts.map +1 -0
  95. package/dist/server/services/password-reset.service.js +54 -0
  96. package/dist/server/services/password-reset.service.js.map +1 -0
  97. package/dist/server/services/super-admin.service.d.ts +59 -0
  98. package/dist/server/services/super-admin.service.d.ts.map +1 -0
  99. package/dist/server/services/super-admin.service.js +187 -0
  100. package/dist/server/services/super-admin.service.js.map +1 -0
  101. package/dist/server/types.d.ts +186 -0
  102. package/dist/server/types.d.ts.map +1 -0
  103. package/dist/server/types.js +6 -0
  104. package/dist/server/types.js.map +1 -0
  105. package/dist/shared/types.d.ts +23 -0
  106. package/dist/shared/types.d.ts.map +1 -0
  107. package/dist/shared/types.js +6 -0
  108. package/dist/shared/types.js.map +1 -0
  109. package/package.json +56 -0
  110. package/src/client/api.ts +314 -0
  111. package/src/client/components/AuditEventRow.tsx +59 -0
  112. package/src/client/components/CascadePreview.tsx +36 -0
  113. package/src/client/components/DangerZoneCard.tsx +103 -0
  114. package/src/client/components/InvitationCodeDisplay.tsx +48 -0
  115. package/src/client/components/InviteForm.tsx +77 -0
  116. package/src/client/components/MemberRow.tsx +69 -0
  117. package/src/client/components/PendingTransferBanner.tsx +98 -0
  118. package/src/client/components/PlaceholderCard.tsx +26 -0
  119. package/src/client/components/RoleBadge.tsx +26 -0
  120. package/src/client/components/RoleSelect.tsx +35 -0
  121. package/src/client/hooks/.gitkeep +0 -0
  122. package/src/client/hooks/useCurrentMembership.ts +24 -0
  123. package/src/client/hooks/useMembers.ts +24 -0
  124. package/src/client/hooks/usePendingInvitations.ts +24 -0
  125. package/src/client/hooks/usePendingTransfer.ts +27 -0
  126. package/src/client/index.ts +80 -0
  127. package/src/client/pages/AuditLogPage.tsx +164 -0
  128. package/src/client/pages/EmailChangePage.tsx +144 -0
  129. package/src/client/pages/InvitationAcceptPage.tsx +163 -0
  130. package/src/client/pages/InvitationCodePage.tsx +108 -0
  131. package/src/client/pages/MembersPage.tsx +290 -0
  132. package/src/client/pages/OrgSettingsPage.tsx +185 -0
  133. package/src/client/pages/OwnershipTransferPage.tsx +163 -0
  134. package/src/client/pages/PasswordResetPage.tsx +104 -0
  135. package/src/client/pages/PasswordResetRequestPage.tsx +71 -0
  136. package/src/client/pages/PlaceholderPage.tsx +20 -0
  137. package/src/client/pages/SuperAdminDashboard.tsx +401 -0
  138. package/src/client/types.ts +78 -0
  139. package/src/index.ts +24 -0
  140. package/src/server/crypto.ts +47 -0
  141. package/src/server/index.ts +167 -0
  142. package/src/server/middleware/require-membership.ts +48 -0
  143. package/src/server/middleware/require-role.ts +19 -0
  144. package/src/server/middleware/require-super-admin.ts +32 -0
  145. package/src/server/migrations/0001_create_tm_schema_migrations.sql +13 -0
  146. package/src/server/migrations/0002_create_tm_organizations.sql +14 -0
  147. package/src/server/migrations/0003_create_tm_memberships.sql +24 -0
  148. package/src/server/migrations/0004_create_tm_invitations.sql +22 -0
  149. package/src/server/migrations/0005_create_tm_audit_events.sql +17 -0
  150. package/src/server/migrations/0006_create_tm_email_change_requests.sql +13 -0
  151. package/src/server/migrations/0007_create_tm_ownership_transfers.sql +22 -0
  152. package/src/server/migrations/0008_create_tm_super_admins.sql +8 -0
  153. package/src/server/migrations/0009_create_tm_password_reset_requests.sql +9 -0
  154. package/src/server/migrations/0010_create_tm_shared_access.sql +8 -0
  155. package/src/server/migrations/0011_seed_super_admin.sql +15 -0
  156. package/src/server/migrations/0012_create_tm_user_locks.sql +7 -0
  157. package/src/server/routes/admin.routes.ts +208 -0
  158. package/src/server/routes/audit.routes.ts +93 -0
  159. package/src/server/routes/health.routes.ts +46 -0
  160. package/src/server/routes/invitations.routes.ts +252 -0
  161. package/src/server/routes/me.routes.ts +143 -0
  162. package/src/server/routes/orgs.routes.ts +428 -0
  163. package/src/server/routes/transfer.routes.ts +110 -0
  164. package/src/server/services/.gitkeep +0 -0
  165. package/src/server/services/audit.service.ts +49 -0
  166. package/src/server/services/email-change.service.ts +178 -0
  167. package/src/server/services/invitations.service.ts +316 -0
  168. package/src/server/services/memberships.service.ts +129 -0
  169. package/src/server/services/organizations.service.ts +110 -0
  170. package/src/server/services/ownership.service.ts +170 -0
  171. package/src/server/services/password-reset.service.ts +94 -0
  172. package/src/server/services/super-admin.service.ts +321 -0
  173. package/src/server/sql/.gitkeep +0 -0
  174. package/src/server/types.ts +145 -0
  175. package/src/shared/types.ts +24 -0
  176. package/tests/integration/audit-fires.test.ts +288 -0
  177. package/tests/integration/cascade-preview.test.ts +157 -0
  178. package/tests/integration/email-change.test.ts +190 -0
  179. package/tests/integration/feature-flags.test.ts +213 -0
  180. package/tests/integration/invitations-code.test.ts +218 -0
  181. package/tests/integration/invitations-expiry.test.ts +216 -0
  182. package/tests/integration/invitations-resend.test.ts +241 -0
  183. package/tests/integration/invitations-revoke.test.ts +226 -0
  184. package/tests/integration/invitations-switch-org.test.ts +156 -0
  185. package/tests/integration/invitations-token.test.ts +221 -0
  186. package/tests/integration/migrations.test.ts +119 -0
  187. package/tests/integration/only-owner-protections.test.ts +130 -0
  188. package/tests/integration/org-lifecycle.test.ts +169 -0
  189. package/tests/integration/ownership-transfer-cancel.test.ts +171 -0
  190. package/tests/integration/ownership-transfer-expire.test.ts +171 -0
  191. package/tests/integration/ownership-transfer-happy.test.ts +184 -0
  192. package/tests/integration/ownership-transfer-locks.test.ts +146 -0
  193. package/tests/integration/password-reset.test.ts +200 -0
  194. package/tests/integration/super-admin-actions.test.ts +180 -0
  195. package/tests/integration/super-admin-restrictions.test.ts +209 -0
  196. package/tests/setup/global-setup.ts +20 -0
  197. package/tests/unit/adapter-shape.test.ts +330 -0
  198. package/tests/unit/role-permissions.test.ts +236 -0
  199. package/tests/unit/validation.test.ts +304 -0
  200. package/tsconfig.client.json +13 -0
  201. package/tsconfig.json +12 -0
  202. package/tsconfig.tsbuildinfo +1 -0
  203. package/vitest.config.ts +13 -0
@@ -0,0 +1,371 @@
1
+ import { Router } from 'express';
2
+ import { requireMembership } from '../middleware/require-membership.js';
3
+ import { requireRole } from '../middleware/require-role.js';
4
+ import { getOrg, updateOrg, softDeleteOrg, listOrgMembers } from '../services/organizations.service.js';
5
+ import { removeMember, changeRole, validateRoleChange } from '../services/memberships.service.js';
6
+ import { getPendingTransfer } from '../services/ownership.service.js';
7
+ import { writeAuditEvent, getClientIp } from '../services/audit.service.js';
8
+ export function createOrgsRouter(pool, adapter, flags) {
9
+ const router = Router({ mergeParams: true });
10
+ const authMiddleware = requireMembership(pool, adapter);
11
+ // POST /orgs — create org (any authenticated user)
12
+ router.post('/', async (req, res) => {
13
+ try {
14
+ const userId = await adapter.getCurrentUserId(req);
15
+ if (!userId) {
16
+ res.status(401).json({ error: 'Authentication required' });
17
+ return;
18
+ }
19
+ const { name, slug, settings } = req.body;
20
+ if (!name || !slug) {
21
+ res.status(400).json({ error: 'name and slug are required' });
22
+ return;
23
+ }
24
+ const result = await pool.query(`INSERT INTO tm_organizations (name, slug, owner_user_id, settings)
25
+ VALUES ($1, $2, $3, $4) RETURNING *`, [name, slug, userId, JSON.stringify(settings ?? {})]);
26
+ const org = result.rows[0];
27
+ await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role, joined_at)
28
+ VALUES ($1, $2, 'owner', NOW())`, [org.id, userId]);
29
+ if (flags.enableAuditLog) {
30
+ await writeAuditEvent({
31
+ pool,
32
+ orgId: org.id,
33
+ actorUserId: userId,
34
+ action: 'org.created',
35
+ targetType: 'org',
36
+ targetId: org.id,
37
+ after: { name, slug },
38
+ ip: getClientIp(req),
39
+ userAgent: req.headers['user-agent'] ?? null,
40
+ });
41
+ }
42
+ res.status(201).json({ org });
43
+ }
44
+ catch (e) {
45
+ const msg = e.message;
46
+ adapter.logger.error('[orgs] POST /', { error: msg });
47
+ if (msg.includes('unique') || msg.includes('duplicate') || msg.includes('already exists')) {
48
+ res.status(409).json({ error: 'Organization with that slug already exists' });
49
+ }
50
+ else {
51
+ res.status(500).json({ error: 'Failed to create organization' });
52
+ }
53
+ }
54
+ });
55
+ // GET /orgs/:orgId — org info (member+)
56
+ router.get('/:orgId', authMiddleware, async (req, res) => {
57
+ const { orgId } = req;
58
+ try {
59
+ const org = await getOrg(pool, orgId);
60
+ if (!org) {
61
+ res.status(404).json({ error: 'Organization not found' });
62
+ return;
63
+ }
64
+ res.json({ org });
65
+ }
66
+ catch (e) {
67
+ adapter.logger.error('[orgs] GET /:orgId', { error: e.message });
68
+ res.status(500).json({ error: 'Failed to fetch organization' });
69
+ }
70
+ });
71
+ // PATCH /orgs/:orgId — update name/slug/settings (admin+)
72
+ router.patch('/:orgId', authMiddleware, requireRole('admin'), async (req, res) => {
73
+ const { orgId, userId } = req;
74
+ const { name, slug } = req.body;
75
+ try {
76
+ const before = await getOrg(pool, orgId);
77
+ const updated = await updateOrg(pool, orgId, { name, slug });
78
+ if (flags.enableAuditLog) {
79
+ await writeAuditEvent({
80
+ pool,
81
+ orgId,
82
+ actorUserId: userId,
83
+ action: 'org.settings.updated',
84
+ targetType: 'org',
85
+ targetId: orgId,
86
+ before: { name: before?.name, slug: before?.slug },
87
+ after: { name: updated.name, slug: updated.slug },
88
+ ip: getClientIp(req),
89
+ userAgent: req.headers['user-agent'] ?? null,
90
+ });
91
+ }
92
+ res.json({ org: updated });
93
+ }
94
+ catch (e) {
95
+ adapter.logger.error('[orgs] PATCH /:orgId', { error: e.message });
96
+ res.status(500).json({ error: 'Failed to update organization' });
97
+ }
98
+ });
99
+ // DELETE /orgs/:orgId — soft delete (owner only), requires confirmName
100
+ router.delete('/:orgId', authMiddleware, requireRole('owner'), async (req, res) => {
101
+ const { orgId, userId } = req;
102
+ // Accept both confirmName and confirmOrgName for compatibility
103
+ const { confirmName, confirmOrgName } = req.body;
104
+ const confirm = confirmName ?? confirmOrgName;
105
+ try {
106
+ const org = await getOrg(pool, orgId);
107
+ if (!org) {
108
+ res.status(404).json({ error: 'Organization not found' });
109
+ return;
110
+ }
111
+ if (!confirm || confirm !== org.name) {
112
+ res.status(422).json({ error: 'Confirmation name does not match organization name' });
113
+ return;
114
+ }
115
+ // Transfer lock: cannot delete org while ownership transfer is pending
116
+ const pendingTransfer = await getPendingTransfer(pool, orgId);
117
+ if (pendingTransfer) {
118
+ res.status(409).json({ error: 'Cannot delete organization while an ownership transfer is pending. Cancel the transfer first.' });
119
+ return;
120
+ }
121
+ await softDeleteOrg(pool, orgId, userId);
122
+ if (flags.enableAuditLog) {
123
+ await writeAuditEvent({
124
+ pool,
125
+ orgId,
126
+ actorUserId: userId,
127
+ action: 'org.deleted',
128
+ targetType: 'org',
129
+ targetId: orgId,
130
+ ip: getClientIp(req),
131
+ userAgent: req.headers['user-agent'] ?? null,
132
+ });
133
+ }
134
+ try {
135
+ const members = await listOrgMembers(pool, orgId, { includeRemoved: false });
136
+ const userIds = members.map(m => m.user_id);
137
+ const users = await adapter.getUsersByIds(userIds);
138
+ const scheduledFor = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
139
+ for (const user of users) {
140
+ await adapter.sendOrgDeletionNotice({
141
+ to: user.email,
142
+ orgName: org.name,
143
+ scheduledFor,
144
+ });
145
+ }
146
+ }
147
+ catch (e) {
148
+ adapter.logger.warn('[orgs] Failed to send deletion notices', { error: e.message });
149
+ }
150
+ res.json({ message: 'Organization scheduled for deletion in 30 days' });
151
+ }
152
+ catch (e) {
153
+ adapter.logger.error('[orgs] DELETE /:orgId', { error: e.message });
154
+ res.status(500).json({ error: 'Failed to delete organization' });
155
+ }
156
+ });
157
+ // GET /orgs/:orgId/members — list members (member+)
158
+ router.get('/:orgId/members', authMiddleware, async (req, res) => {
159
+ const { orgId } = req;
160
+ try {
161
+ const members = await listOrgMembers(pool, orgId);
162
+ const userIds = members.map(m => m.user_id);
163
+ const users = await adapter.getUsersByIds(userIds);
164
+ const userMap = new Map(users.map(u => [u.id, u]));
165
+ const enriched = members.map(m => ({ ...m, user: userMap.get(m.user_id) }));
166
+ res.json({ members: enriched });
167
+ }
168
+ catch (e) {
169
+ adapter.logger.error('[orgs] GET /:orgId/members', { error: e.message });
170
+ res.status(500).json({ error: 'Failed to fetch members' });
171
+ }
172
+ });
173
+ // GET /orgs/:orgId/members/former — former members (admin+)
174
+ router.get('/:orgId/members/former', authMiddleware, requireRole('admin'), async (req, res) => {
175
+ const { orgId } = req;
176
+ try {
177
+ const allMembers = await listOrgMembers(pool, orgId, { includeRemoved: true });
178
+ const former = allMembers.filter(m => m.removed_at !== null);
179
+ res.json({ members: former });
180
+ }
181
+ catch (e) {
182
+ adapter.logger.error('[orgs] GET /:orgId/members/former', { error: e.message });
183
+ res.status(500).json({ error: 'Failed to fetch former members' });
184
+ }
185
+ });
186
+ // DELETE /orgs/:orgId/members/:userId — remove member (admin+)
187
+ router.delete('/:orgId/members/:userId', authMiddleware, requireRole('admin'), async (req, res) => {
188
+ const { orgId, userId: actorId, userRole } = req;
189
+ const targetUserId = parseInt(req.params.userId, 10);
190
+ const { reason } = req.body;
191
+ if (isNaN(targetUserId)) {
192
+ res.status(400).json({ error: 'Invalid user ID' });
193
+ return;
194
+ }
195
+ if (targetUserId === actorId) {
196
+ res.status(400).json({ message: 'Cannot remove yourself: you are the owner of this organization' });
197
+ return;
198
+ }
199
+ try {
200
+ const targetMemberResult = await pool.query(`SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`, [orgId, targetUserId]);
201
+ if (targetMemberResult.rows.length === 0) {
202
+ res.status(404).json({ error: 'Member not found' });
203
+ return;
204
+ }
205
+ const targetRole = targetMemberResult.rows[0].role;
206
+ if (userRole === 'admin' && (targetRole === 'owner' || targetRole === 'admin')) {
207
+ res.status(403).json({ error: 'Admins cannot remove owners or other admins' });
208
+ return;
209
+ }
210
+ // Transfer lock: cannot remove a user involved in a pending transfer
211
+ const pendingTransferForRemove = await getPendingTransfer(pool, orgId);
212
+ if (pendingTransferForRemove &&
213
+ (pendingTransferForRemove.from_user_id === targetUserId ||
214
+ pendingTransferForRemove.to_user_id === targetUserId)) {
215
+ res.status(409).json({ error: 'Cannot remove a member involved in a pending ownership transfer. Cancel the transfer first.' });
216
+ return;
217
+ }
218
+ await removeMember(pool, { orgId, userId: targetUserId, removedByUserId: actorId, reason });
219
+ if (flags.enableAuditLog) {
220
+ await writeAuditEvent({
221
+ pool,
222
+ orgId,
223
+ actorUserId: actorId,
224
+ action: 'member.removed',
225
+ targetType: 'user',
226
+ targetId: targetUserId,
227
+ before: { role: targetRole },
228
+ reason: reason ?? null,
229
+ ip: getClientIp(req),
230
+ userAgent: req.headers['user-agent'] ?? null,
231
+ });
232
+ }
233
+ res.json({ message: 'Member removed successfully' });
234
+ }
235
+ catch (e) {
236
+ adapter.logger.error('[orgs] DELETE /:orgId/members/:userId', { error: e.message });
237
+ res.status(500).json({ error: 'Failed to remove member' });
238
+ }
239
+ });
240
+ // PATCH /orgs/:orgId/members/:userId/role — change role (admin+)
241
+ router.patch('/:orgId/members/:userId/role', authMiddleware, requireRole('admin'), async (req, res) => {
242
+ const { orgId, userId: actorId, userRole } = req;
243
+ const targetUserId = parseInt(req.params.userId, 10);
244
+ const { role: newRole } = req.body;
245
+ if (isNaN(targetUserId)) {
246
+ res.status(400).json({ error: 'Invalid user ID' });
247
+ return;
248
+ }
249
+ if (!newRole) {
250
+ res.status(400).json({ error: 'role is required' });
251
+ return;
252
+ }
253
+ try {
254
+ await validateRoleChange(pool, { orgId, actorRole: userRole, targetUserId, newRole: newRole });
255
+ // Transfer lock: cannot change role of a user involved in a pending transfer
256
+ const pendingTransferForPatch = await getPendingTransfer(pool, orgId);
257
+ if (pendingTransferForPatch &&
258
+ (pendingTransferForPatch.from_user_id === targetUserId ||
259
+ pendingTransferForPatch.to_user_id === targetUserId)) {
260
+ res.status(409).json({ error: 'Cannot change role of a member involved in a pending ownership transfer. Cancel the transfer first.' });
261
+ return;
262
+ }
263
+ const before = await pool.query(`SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`, [orgId, targetUserId]);
264
+ const updated = await changeRole(pool, { orgId, userId: targetUserId, newRole: newRole, changedByUserId: actorId });
265
+ if (flags.enableAuditLog) {
266
+ await writeAuditEvent({
267
+ pool,
268
+ orgId,
269
+ actorUserId: actorId,
270
+ action: 'member.role_changed',
271
+ targetType: 'user',
272
+ targetId: targetUserId,
273
+ before: { role: before.rows[0]?.role },
274
+ after: { role: newRole },
275
+ ip: getClientIp(req),
276
+ userAgent: req.headers['user-agent'] ?? null,
277
+ });
278
+ }
279
+ res.json({ membership: updated });
280
+ }
281
+ catch (e) {
282
+ const msg = e.message;
283
+ adapter.logger.error('[orgs] PATCH /:orgId/members/:userId/role', { error: msg });
284
+ if (msg.includes('Cannot') || msg.includes('Requires') || msg.includes('cannot')) {
285
+ res.status(403).json({ error: msg });
286
+ }
287
+ else {
288
+ res.status(500).json({ error: 'Failed to change role' });
289
+ }
290
+ }
291
+ });
292
+ // PATCH /orgs/:orgId/members/:userId — alias without /role suffix (for compatibility)
293
+ router.patch('/:orgId/members/:userId', authMiddleware, requireRole('admin'), async (req, res) => {
294
+ const { orgId, userId: actorId, userRole } = req;
295
+ const targetUserId = parseInt(req.params.userId, 10);
296
+ const { role: newRole } = req.body;
297
+ if (isNaN(targetUserId)) {
298
+ res.status(400).json({ error: 'Invalid user ID' });
299
+ return;
300
+ }
301
+ if (!newRole) {
302
+ res.status(400).json({ error: 'role is required' });
303
+ return;
304
+ }
305
+ try {
306
+ await validateRoleChange(pool, { orgId, actorRole: userRole, targetUserId, newRole: newRole });
307
+ // Transfer lock: cannot change role of a user involved in a pending transfer
308
+ const pendingTransferForPatch = await getPendingTransfer(pool, orgId);
309
+ if (pendingTransferForPatch &&
310
+ (pendingTransferForPatch.from_user_id === targetUserId ||
311
+ pendingTransferForPatch.to_user_id === targetUserId)) {
312
+ res.status(409).json({ error: 'Cannot change role of a member involved in a pending ownership transfer. Cancel the transfer first.' });
313
+ return;
314
+ }
315
+ const before = await pool.query(`SELECT role FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`, [orgId, targetUserId]);
316
+ const updated = await changeRole(pool, { orgId, userId: targetUserId, newRole: newRole, changedByUserId: actorId });
317
+ if (flags.enableAuditLog) {
318
+ await writeAuditEvent({
319
+ pool,
320
+ orgId,
321
+ actorUserId: actorId,
322
+ action: 'member.role_changed',
323
+ targetType: 'user',
324
+ targetId: targetUserId,
325
+ before: { role: before.rows[0]?.role },
326
+ after: { role: newRole },
327
+ ip: getClientIp(req),
328
+ userAgent: req.headers['user-agent'] ?? null,
329
+ });
330
+ }
331
+ res.json({ membership: updated });
332
+ }
333
+ catch (e) {
334
+ const msg = e.message;
335
+ adapter.logger.error('[orgs] PATCH /:orgId/members/:userId', { error: msg });
336
+ if (msg.includes('Cannot') || msg.includes('Requires') || msg.includes('cannot')) {
337
+ res.status(403).json({ error: msg });
338
+ }
339
+ else {
340
+ res.status(500).json({ error: 'Failed to change role' });
341
+ }
342
+ }
343
+ });
344
+ // GET /orgs/:orgId/members/:userId/cascade-preview — preview cascade effects of removing a member (admin+)
345
+ router.get('/:orgId/members/:userId/cascade-preview', authMiddleware, requireRole('admin'), async (req, res) => {
346
+ const { orgId } = req;
347
+ const targetUserId = parseInt(req.params.userId, 10);
348
+ if (isNaN(targetUserId)) {
349
+ res.status(400).json({ error: 'Invalid user ID' });
350
+ return;
351
+ }
352
+ try {
353
+ const membershipResult = await pool.query(`SELECT * FROM tm_memberships WHERE org_id = $1 AND user_id = $2 AND removed_at IS NULL`, [orgId, targetUserId]);
354
+ if (membershipResult.rows.length === 0) {
355
+ res.status(404).json({ error: 'Member not found' });
356
+ return;
357
+ }
358
+ const membership = membershipResult.rows[0];
359
+ const invitationsResult = await pool.query(`SELECT * FROM tm_invitations
360
+ WHERE org_id = $1 AND invited_by_user_id = $2
361
+ AND revoked_at IS NULL AND accepted_at IS NULL AND expires_at > NOW()`, [orgId, targetUserId]);
362
+ res.json({ membership, pendingInvitations: invitationsResult.rows });
363
+ }
364
+ catch (e) {
365
+ adapter.logger.error('[orgs] GET cascade-preview', { error: e.message });
366
+ res.status(500).json({ error: 'Failed to fetch cascade preview' });
367
+ }
368
+ });
369
+ return router;
370
+ }
371
+ //# sourceMappingURL=orgs.routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgs.routes.js","sourceRoot":"","sources":["../../../src/server/routes/orgs.routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import type { Pool } from 'pg';
3
+ import type { ServerModuleAdapter, TeamManagementFeatureFlags } from '../types.js';
4
+ export declare function createTransferRouter(pool: Pool, adapter: ServerModuleAdapter, flags: TeamManagementFeatureFlags, baseUrl: string): Router;
5
+ //# sourceMappingURL=transfer.routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer.routes.d.ts","sourceRoot":"","sources":["../../../src/server/routes/transfer.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAWnF,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,MAAM,GACd,MAAM,CA2FR"}
@@ -0,0 +1,108 @@
1
+ import { Router } from 'express';
2
+ import { requireMembership } from '../middleware/require-membership.js';
3
+ import { requireRole } from '../middleware/require-role.js';
4
+ import { initiateTransfer, acceptTransfer, cancelTransfer, getPendingTransfer, } from '../services/ownership.service.js';
5
+ import { writeAuditEvent, getClientIp } from '../services/audit.service.js';
6
+ export function createTransferRouter(pool, adapter, flags, baseUrl) {
7
+ const router = Router({ mergeParams: true });
8
+ const authMiddleware = requireMembership(pool, adapter);
9
+ function featureCheck(res) {
10
+ if (!flags.enableOwnershipTransfer) {
11
+ res.status(501).json({ error: 'Ownership transfer is not enabled' });
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ router.get('/:orgId/transfer', authMiddleware, async (req, res) => {
17
+ if (!featureCheck(res))
18
+ return;
19
+ const { orgId } = req;
20
+ try {
21
+ const transfer = await getPendingTransfer(pool, orgId);
22
+ res.json({ transfer });
23
+ }
24
+ catch (e) {
25
+ adapter.logger.error('[transfer] GET', { error: e.message });
26
+ res.status(500).json({ error: 'Failed to fetch transfer' });
27
+ }
28
+ });
29
+ router.post('/:orgId/transfer', authMiddleware, requireRole('owner'), async (req, res) => {
30
+ if (!featureCheck(res))
31
+ return;
32
+ const { orgId, userId } = req;
33
+ const { toUserId } = req.body;
34
+ if (!toUserId) {
35
+ res.status(400).json({ error: 'toUserId is required' });
36
+ return;
37
+ }
38
+ try {
39
+ const transfer = await initiateTransfer(pool, adapter, { orgId, fromUserId: userId, toUserId, baseUrl });
40
+ if (flags.enableAuditLog) {
41
+ await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_initiated',
42
+ targetType: 'user', targetId: toUserId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
43
+ }
44
+ res.status(201).json({ transfer });
45
+ }
46
+ catch (e) {
47
+ const msg = e.message;
48
+ adapter.logger.error('[transfer] POST initiate', { error: msg });
49
+ if (msg.includes('not a member') || msg.includes('admin') || msg.includes('pending')) {
50
+ res.status(422).json({ error: msg });
51
+ }
52
+ else {
53
+ res.status(500).json({ error: 'Failed to initiate transfer' });
54
+ }
55
+ }
56
+ });
57
+ router.post('/:orgId/transfer/accept', authMiddleware, async (req, res) => {
58
+ if (!featureCheck(res))
59
+ return;
60
+ const { orgId, userId } = req;
61
+ try {
62
+ await acceptTransfer(pool, adapter, { orgId, acceptingUserId: userId });
63
+ if (flags.enableAuditLog) {
64
+ await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_accepted',
65
+ targetType: 'org', targetId: orgId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
66
+ }
67
+ res.json({ message: 'Ownership transfer accepted. You are now the owner.' });
68
+ }
69
+ catch (e) {
70
+ const msg = e.message;
71
+ adapter.logger.error('[transfer] POST accept', { error: msg });
72
+ if (msg.includes('No valid') || msg.includes('Only the designated')) {
73
+ res.status(422).json({ error: msg });
74
+ }
75
+ else {
76
+ res.status(500).json({ error: 'Failed to accept transfer' });
77
+ }
78
+ }
79
+ });
80
+ router.delete('/:orgId/transfer', authMiddleware, async (req, res) => {
81
+ if (!featureCheck(res))
82
+ return;
83
+ const { orgId, userId, userRole } = req;
84
+ try {
85
+ const pending = await getPendingTransfer(pool, orgId);
86
+ if (!pending) {
87
+ res.status(404).json({ error: 'No pending transfer found' });
88
+ return;
89
+ }
90
+ if (userRole !== 'owner' && pending.to_user_id !== userId) {
91
+ res.status(403).json({ error: 'Only the initiating owner or designated recipient can cancel this transfer' });
92
+ return;
93
+ }
94
+ await cancelTransfer(pool, { orgId, cancelledByUserId: userId });
95
+ if (flags.enableAuditLog) {
96
+ await writeAuditEvent({ pool, orgId, actorUserId: userId, action: 'ownership.transfer_cancelled',
97
+ targetType: 'org', targetId: orgId, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null });
98
+ }
99
+ res.json({ message: 'Transfer cancelled' });
100
+ }
101
+ catch (e) {
102
+ adapter.logger.error('[transfer] DELETE cancel', { error: e.message });
103
+ res.status(500).json({ error: 'Failed to cancel transfer' });
104
+ }
105
+ });
106
+ return router;
107
+ }
108
+ //# sourceMappingURL=transfer.routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer.routes.js","sourceRoot":"","sources":["../../../src/server/routes/transfer.routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { Pool, PoolClient } from 'pg';
2
+ import type { AuditActorType } from '../types.js';
3
+ interface WriteAuditEventParams {
4
+ pool: Pool | PoolClient;
5
+ orgId: number | null;
6
+ actorUserId: number | null;
7
+ actorType?: AuditActorType;
8
+ action: string;
9
+ targetType?: string | null;
10
+ targetId?: string | number | null;
11
+ before?: Record<string, unknown> | null;
12
+ after?: Record<string, unknown> | null;
13
+ ip?: string | null;
14
+ userAgent?: string | null;
15
+ reason?: string | null;
16
+ }
17
+ export declare function writeAuditEvent(params: WriteAuditEventParams): Promise<void>;
18
+ export declare function getClientIp(req: import('express').Request): string;
19
+ export {};
20
+ //# sourceMappingURL=audit.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.service.d.ts","sourceRoot":"","sources":["../../../src/server/services/audit.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,UAAU,qBAAqB;IAC7B,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BlF;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,SAAS,EAAE,OAAO,GAAG,MAAM,CAElE"}
@@ -0,0 +1,23 @@
1
+ export async function writeAuditEvent(params) {
2
+ const { pool, orgId, actorUserId, actorType = 'user', action, targetType = null, targetId = null, before = null, after = null, ip = null, userAgent = null, reason = null, } = params;
3
+ await pool.query(`INSERT INTO tm_audit_events
4
+ (org_id, actor_user_id, actor_type, action, target_type, target_id,
5
+ before_state, after_state, ip, user_agent, reason)
6
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, [
7
+ orgId,
8
+ actorUserId,
9
+ actorType,
10
+ action,
11
+ targetType,
12
+ targetId !== null ? String(targetId) : null,
13
+ before ? JSON.stringify(before) : null,
14
+ after ? JSON.stringify(after) : null,
15
+ ip,
16
+ userAgent,
17
+ reason,
18
+ ]);
19
+ }
20
+ export function getClientIp(req) {
21
+ return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ?? req.socket.remoteAddress ?? 'unknown';
22
+ }
23
+ //# sourceMappingURL=audit.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.service.js","sourceRoot":"","sources":["../../../src/server/services/audit.service.ts"],"names":[],"mappings":"AAkBA,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAA6B;IACjE,MAAM,EACJ,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,GAAG,MAAM,EAAE,MAAM,EACpD,UAAU,GAAG,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,EAC/D,EAAE,GAAG,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,GAC3C,GAAG,MAAM,CAAC;IAEX,MAAO,IAAa,CAAC,KAAK,CACxB;;;iDAG6C,EAC7C;QACE,KAAK;QACL,WAAW;QACX,SAAS;QACT,MAAM;QACN,UAAU;QACV,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;QAC3C,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI;QACtC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;QACpC,EAAE;QACF,SAAS;QACT,MAAM;KACP,CACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAA8B;IACxD,OAAQ,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAY,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAC;AACpH,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Pool } from 'pg';
2
+ import type { ServerModuleAdapter } from '../types.js';
3
+ export declare function requestEmailChange(pool: Pool, adapter: ServerModuleAdapter, { userId, currentEmail, newEmail, baseUrl, }: {
4
+ userId: number;
5
+ currentEmail: string;
6
+ newEmail: string;
7
+ baseUrl: string;
8
+ }): Promise<void>;
9
+ export declare function verifyEmailChange(pool: Pool, adapter: ServerModuleAdapter, { token, userId: _userId }: {
10
+ token: string;
11
+ userId?: number | null;
12
+ }): Promise<void>;
13
+ export declare function cancelEmailChange(pool: Pool, adapter: ServerModuleAdapter, { token }: {
14
+ token: string;
15
+ }): Promise<void>;
16
+ //# sourceMappingURL=email-change.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-change.service.d.ts","sourceRoot":"","sources":["../../../src/server/services/email-change.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAoBvD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,EACE,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,OAAO,GACR,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,IAAI,CAAC,CAsDf;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqBf"}