@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,145 +0,0 @@
1
- import type { Request } from 'express';
2
-
3
- export type OrgRole = 'owner' | 'admin' | 'member' | 'viewer';
4
- export type AuditActorType = 'user' | 'super_admin';
5
- export type TransferStatus = 'pending' | 'accepted' | 'cancelled' | 'expired';
6
-
7
- export interface TmOrganization {
8
- id: number;
9
- name: string;
10
- slug: string;
11
- owner_user_id: number;
12
- settings: Record<string, unknown>;
13
- deleted_at: Date | null;
14
- delete_scheduled_for: Date | null;
15
- deleted_by_user_id: number | null;
16
- created_at: Date;
17
- updated_at: Date;
18
- }
19
-
20
- export interface TmMembership {
21
- id: number;
22
- org_id: number;
23
- user_id: number;
24
- role: OrgRole;
25
- joined_at: Date;
26
- removed_at: Date | null;
27
- removed_by_user_id: number | null;
28
- removal_reason: string | null;
29
- created_at: Date;
30
- updated_at: Date;
31
- }
32
-
33
- export interface TmInvitation {
34
- id: number;
35
- org_id: number;
36
- invited_by_user_id: number;
37
- email: string;
38
- role: OrgRole;
39
- token_hash: string;
40
- code_encrypted: string;
41
- expires_at: Date;
42
- accepted_at: Date | null;
43
- revoked_at: Date | null;
44
- revoked_by_user_id: number | null;
45
- resent_count: number;
46
- created_at: Date;
47
- updated_at: Date;
48
- }
49
-
50
- export interface TmAuditEvent {
51
- id: string;
52
- org_id: number | null;
53
- actor_user_id: number | null;
54
- actor_type: AuditActorType;
55
- action: string;
56
- target_type: string | null;
57
- target_id: string | null;
58
- before_state: Record<string, unknown> | null;
59
- after_state: Record<string, unknown> | null;
60
- ip: string | null;
61
- user_agent: string | null;
62
- reason: string | null;
63
- created_at: Date;
64
- }
65
-
66
- export interface TmOwnershipTransfer {
67
- id: number;
68
- org_id: number;
69
- from_user_id: number;
70
- to_user_id: number;
71
- status: TransferStatus;
72
- initiated_at: Date;
73
- accepted_at: Date | null;
74
- cancelled_at: Date | null;
75
- cancelled_by_user_id: number | null;
76
- expires_at: Date;
77
- }
78
-
79
- export interface TmPasswordResetRequest {
80
- id: number;
81
- user_id: number;
82
- token_hash: string;
83
- expires_at: Date;
84
- used_at: Date | null;
85
- created_at: Date;
86
- }
87
-
88
- export interface ServerModuleAdapter {
89
- // v0.0.1
90
- getCurrentUserId(req: Request): Promise<number | null>;
91
- getOrganizationIdForUser(userId: number): Promise<number | null>;
92
- isUserOrgAdmin(userId: number, orgId: number): Promise<boolean>;
93
- logger: {
94
- info(message: string, meta?: Record<string, unknown>): void;
95
- warn(message: string, meta?: Record<string, unknown>): void;
96
- error(message: string, meta?: Record<string, unknown>): void;
97
- };
98
- // v0.1.0 additions
99
- getUserById(userId: number): Promise<{ id: number; email: string; name?: string } | null>;
100
- getUsersByIds(userIds: number[]): Promise<Array<{ id: number; email: string; name?: string }>>;
101
- findUserByEmail(email: string): Promise<{ id: number; email: string } | null>;
102
- createUserFromInvite(data: { email: string; orgId: number; role: OrgRole }): Promise<{ id: number; email: string }>;
103
- setUserPassword(userId: number, passwordHash: string): Promise<void>;
104
- hashPassword(plaintext: string): Promise<string>;
105
- verifyPassword(plaintext: string, hash: string): Promise<boolean>;
106
- invalidateAllUserSessions(userId: number): Promise<void>;
107
- sendInviteEmail(data: { to: string; orgName: string; inviterName: string; role: OrgRole; magicLinkUrl: string; code: string }): Promise<void>;
108
- sendOwnershipTransferEmail(data: { to: string; orgName: string; fromName: string; transferUrl: string }): Promise<void>;
109
- sendEmailChangeVerification(data: { to: string; verifyUrl: string }): Promise<void>;
110
- sendEmailChangeOldNotice(data: { to: string; newEmail: string; cancelUrl: string }): Promise<void>;
111
- sendEmailChangedFinalNotice(data: { to: string; oldEmail: string; newEmail: string }): Promise<void>;
112
- sendPasswordResetEmail(data: { to: string; resetUrl: string }): Promise<void>;
113
- sendOrgDeletionNotice(data: { to: string; orgName: string; scheduledFor: Date }): Promise<void>;
114
- emitNotification(data: { userId: number; type: string; payload: Record<string, unknown> }): Promise<void>;
115
- }
116
-
117
- export interface TeamManagementFeatureFlags {
118
- enableInvites?: boolean;
119
- enableAuditLog?: boolean;
120
- enableOwnershipTransfer?: boolean;
121
- enableEmailChange?: boolean;
122
- enablePasswordReset?: boolean;
123
- enableSuperAdmin?: boolean;
124
- enableSharedAccess?: boolean;
125
- enableHardDelete?: boolean;
126
- }
127
-
128
- export interface TeamManagementConfig {
129
- featureFlags?: TeamManagementFeatureFlags;
130
- baseUrl?: string;
131
- }
132
-
133
- export interface TeamManagementServerModule {
134
- router: import('express').Router;
135
- runMigrations(): Promise<{ applied: string[]; skipped: string[] }>;
136
- /** Alias for runMigrations — for test convenience */
137
- migrate(): Promise<{ applied: string[]; skipped: string[] }>;
138
- }
139
-
140
- // Role permission helpers — values are spaced by 10 so future roles can be inserted
141
- export const ROLE_HIERARCHY: Record<OrgRole, number> = { viewer: 10, member: 20, admin: 30, owner: 40 };
142
-
143
- export function roleAtLeast(userRole: OrgRole, required: OrgRole): boolean {
144
- return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[required];
145
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Shared types used by both server and client sides of team-management.
3
- * No Node.js or browser-specific APIs here.
4
- */
5
-
6
- /** A team member as returned by the module's public API. */
7
- export interface TeamMember {
8
- id: number;
9
- userId: number;
10
- organizationId: number;
11
- role: 'owner' | 'admin' | 'member';
12
- joinedAt: string; // ISO 8601
13
- }
14
-
15
- /** An invite as returned by the module's public API (stub shape). */
16
- export interface TeamInvite {
17
- id: number;
18
- organizationId: number;
19
- email: string;
20
- role: 'admin' | 'member';
21
- status: 'pending' | 'accepted' | 'expired';
22
- createdAt: string; // ISO 8601
23
- expiresAt: string; // ISO 8601
24
- }
@@ -1,288 +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 describeWithDb = process.env.DATABASE_URL ? describe : describe.skip;
9
-
10
- let currentUserId: number | null = null;
11
- let currentOrgId: number | null = null;
12
-
13
- function extractToken(spy: ReturnType<typeof vi.fn>): string {
14
- const callArg = spy.mock.calls[0]?.[0] as { magicLinkUrl?: string } | undefined;
15
- const url = callArg?.magicLinkUrl ?? '';
16
- const qs = url.includes('?') ? url.split('?')[1] : '';
17
- return new URLSearchParams(qs).get('token') ?? '';
18
- }
19
-
20
- function extractEmailChangeToken(spy: ReturnType<typeof vi.fn>): string {
21
- const callArg = spy.mock.calls[0]?.[0] as { verifyUrl?: string } | undefined;
22
- const url = callArg?.verifyUrl ?? '';
23
- const qs = url.includes('?') ? url.split('?')[1] : '';
24
- return new URLSearchParams(qs).get('token') ?? '';
25
- }
26
-
27
- const sendInviteEmail = vi.fn(async () => {});
28
- const sendEmailChangeVerification = vi.fn(async () => {});
29
-
30
- const testAdapter: ServerModuleAdapter = {
31
- getCurrentUserId: async () => currentUserId,
32
- getOrganizationIdForUser: async () => currentOrgId,
33
- isUserOrgAdmin: async (userId) => userId <= 2,
34
- logger: { info: () => {}, warn: () => {}, error: () => {} },
35
- getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
36
- getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
37
- findUserByEmail: async (email) => {
38
- const m = email.match(/^u(\d+)@test\.com$/);
39
- return m ? { id: parseInt(m[1]), email } : null;
40
- },
41
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
42
- setUserPassword: async () => {},
43
- hashPassword: async (p) => `h:${p}`,
44
- verifyPassword: async (p, h) => h === `h:${p}`,
45
- invalidateAllUserSessions: async () => {},
46
- sendInviteEmail,
47
- sendOwnershipTransferEmail: async () => {},
48
- sendEmailChangeVerification,
49
- sendEmailChangeOldNotice: async () => {},
50
- sendEmailChangedFinalNotice: async () => {},
51
- sendPasswordResetEmail: async () => {},
52
- sendOrgDeletionNotice: async () => {},
53
- emitNotification: async () => {},
54
- };
55
-
56
- async function cleanAll(pool: Pool) {
57
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
58
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
59
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
60
- }
61
-
62
- async function seedOrg(pool: Pool, orgId = 1) {
63
- await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
64
- VALUES (${orgId}, 'Test Org ${orgId}', 'test-org-${orgId}', 1, '{}') ON CONFLICT DO NOTHING`);
65
- await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role) VALUES
66
- (${orgId}, 1, 'owner'), (${orgId}, 2, 'admin'), (${orgId}, 3, 'member'), (${orgId}, 4, 'viewer')
67
- ON CONFLICT DO NOTHING`);
68
- }
69
-
70
- async function getLatestAuditEvent(pool: Pool, action: string) {
71
- const res = await pool.query(
72
- `SELECT * FROM tm_audit_events WHERE action = $1 ORDER BY created_at DESC LIMIT 1`,
73
- [action]
74
- );
75
- return res.rows[0] ?? null;
76
- }
77
-
78
- describeWithDb('audit events fire for all 13 event types', () => {
79
- let pool: Pool;
80
- let app: express.Express;
81
-
82
- beforeAll(async () => {
83
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
84
- const mod = createServerModule({ adapter: testAdapter, pool, features: {} });
85
- app = express();
86
- app.use(express.json());
87
- app.use(mod.router);
88
- });
89
-
90
- afterAll(async () => {
91
- await pool.end();
92
- });
93
-
94
- beforeEach(async () => {
95
- await cleanAll(pool);
96
- sendInviteEmail.mockClear();
97
- sendEmailChangeVerification.mockClear();
98
- currentUserId = 1;
99
- currentOrgId = 1;
100
- });
101
-
102
- it('org.created fires when org is created', async () => {
103
- const res = await request(app)
104
- .post('/orgs')
105
- .send({ name: 'Audit Org', slug: 'audit-org' });
106
- expect(res.status).toBe(201);
107
-
108
- const event = await getLatestAuditEvent(pool, 'org.created');
109
- expect(event).not.toBeNull();
110
- expect(event.action).toBe('org.created');
111
- });
112
-
113
- it('org.settings.updated fires on PATCH /orgs/:id', async () => {
114
- await seedOrg(pool);
115
-
116
- const res = await request(app)
117
- .patch('/orgs/1')
118
- .send({ name: 'Updated Name' });
119
- expect(res.status).toBeLessThan(500);
120
-
121
- const event = await getLatestAuditEvent(pool, 'org.settings.updated');
122
- expect(event).not.toBeNull();
123
- });
124
-
125
- it('org.deleted fires on DELETE /orgs/:id', async () => {
126
- await seedOrg(pool);
127
-
128
- const res = await request(app)
129
- .delete('/orgs/1')
130
- .send({ confirmName: 'Test Org 1' });
131
- expect(res.status).toBeLessThan(500);
132
-
133
- const event = await getLatestAuditEvent(pool, 'org.deleted');
134
- expect(event).not.toBeNull();
135
- });
136
-
137
- it('member.invited fires on POST /orgs/:id/invitations', async () => {
138
- await seedOrg(pool);
139
-
140
- const res = await request(app)
141
- .post('/orgs/1/invitations')
142
- .send({ email: 'newuser@example.com', role: 'member' });
143
- expect(res.status).toBeLessThan(500);
144
-
145
- const event = await getLatestAuditEvent(pool, 'member.invited');
146
- expect(event).not.toBeNull();
147
- });
148
-
149
- it('member.invite_accepted fires when invitation is accepted', async () => {
150
- await seedOrg(pool);
151
-
152
- const invRes = await request(app)
153
- .post('/orgs/1/invitations')
154
- .send({ email: 'u99@test.com', role: 'member' });
155
- expect(invRes.status).toBeLessThan(500);
156
-
157
- const token = extractToken(sendInviteEmail);
158
- if (!token) return; // Can't test without a token
159
-
160
- currentUserId = 99;
161
- currentOrgId = null;
162
- const acceptRes = await request(app)
163
- .post('/invitations/accept/token')
164
- .send({ token });
165
- expect(acceptRes.status).toBeLessThan(500);
166
-
167
- const event = await getLatestAuditEvent(pool, 'member.invite_accepted');
168
- expect(event).not.toBeNull();
169
- });
170
-
171
- it('member.invite_revoked fires on DELETE /orgs/:id/invitations/:invId', async () => {
172
- await seedOrg(pool);
173
-
174
- const invRes = await request(app)
175
- .post('/orgs/1/invitations')
176
- .send({ email: 'revoke@example.com', role: 'member' });
177
- expect(invRes.status).toBeLessThan(500);
178
-
179
- const inv = await pool.query(`SELECT id FROM tm_invitations ORDER BY created_at DESC LIMIT 1`);
180
- if (!inv.rows.length) return;
181
- const invId = inv.rows[0].id;
182
-
183
- const revokeRes = await request(app).delete(`/orgs/1/invitations/${invId}`);
184
- expect(revokeRes.status).toBeLessThan(500);
185
-
186
- const event = await getLatestAuditEvent(pool, 'member.invite_revoked');
187
- expect(event).not.toBeNull();
188
- });
189
-
190
- it('member.removed fires on DELETE /orgs/:id/members/:userId', async () => {
191
- await seedOrg(pool);
192
-
193
- const res = await request(app).delete('/orgs/1/members/3');
194
- expect(res.status).toBeLessThan(500);
195
-
196
- const event = await getLatestAuditEvent(pool, 'member.removed');
197
- expect(event).not.toBeNull();
198
- });
199
-
200
- it('member.role_changed fires on PATCH /orgs/:id/members/:userId/role', async () => {
201
- await seedOrg(pool);
202
-
203
- const res = await request(app)
204
- .patch('/orgs/1/members/3/role')
205
- .send({ role: 'admin' });
206
- expect(res.status).toBeLessThan(500);
207
-
208
- const event = await getLatestAuditEvent(pool, 'member.role_changed');
209
- expect(event).not.toBeNull();
210
- });
211
-
212
- it('ownership.transfer_initiated fires on POST /orgs/:id/transfer', async () => {
213
- await seedOrg(pool);
214
-
215
- const res = await request(app)
216
- .post('/orgs/1/transfer')
217
- .send({ toUserId: 2 });
218
- expect(res.status).toBeLessThan(500);
219
-
220
- const event = await getLatestAuditEvent(pool, 'ownership.transfer_initiated');
221
- expect(event).not.toBeNull();
222
- });
223
-
224
- it('ownership.transfer_accepted fires on POST /orgs/:id/transfer/accept', async () => {
225
- await seedOrg(pool);
226
-
227
- const initRes = await request(app)
228
- .post('/orgs/1/transfer')
229
- .send({ toUserId: 2 });
230
- expect(initRes.status).toBeLessThan(500);
231
-
232
- currentUserId = 2;
233
- const acceptRes = await request(app)
234
- .post('/orgs/1/transfer/accept')
235
- .send({});
236
- expect(acceptRes.status).toBeLessThan(500);
237
-
238
- const event = await getLatestAuditEvent(pool, 'ownership.transfer_accepted');
239
- expect(event).not.toBeNull();
240
- });
241
-
242
- it('ownership.transfer_cancelled fires on DELETE /orgs/:id/transfer', async () => {
243
- await seedOrg(pool);
244
-
245
- await request(app)
246
- .post('/orgs/1/transfer')
247
- .send({ toUserId: 2 });
248
-
249
- const cancelRes = await request(app)
250
- .delete('/orgs/1/transfer');
251
- expect(cancelRes.status).toBeLessThan(500);
252
-
253
- const event = await getLatestAuditEvent(pool, 'ownership.transfer_cancelled');
254
- expect(event).not.toBeNull();
255
- });
256
-
257
- it('email.change_requested fires on POST /me/email-change', async () => {
258
- await seedOrg(pool);
259
-
260
- const res = await request(app)
261
- .post('/me/email-change')
262
- .send({ newEmail: 'newemail@test.com', currentPassword: 'pass' });
263
- expect(res.status).toBeLessThan(500);
264
-
265
- const event = await getLatestAuditEvent(pool, 'email.change_requested');
266
- expect(event).not.toBeNull();
267
- });
268
-
269
- it('email.change_completed fires when email change is verified', async () => {
270
- await seedOrg(pool);
271
-
272
- await request(app)
273
- .post('/me/email-change')
274
- .send({ newEmail: 'verified@test.com', currentPassword: 'pass' });
275
-
276
- // Extract verify token from spy (service calls sendEmailChangeVerification({ to, verifyUrl }))
277
- const verifyToken = extractEmailChangeToken(sendEmailChangeVerification);
278
- if (!verifyToken) return; // skip if email change not triggered
279
-
280
- currentUserId = null; // token-based route
281
- const verifyRes = await request(app)
282
- .get(`/me/email-change/verify?token=${verifyToken}`);
283
- expect(verifyRes.status).toBeLessThan(500);
284
-
285
- const event = await getLatestAuditEvent(pool, 'email.change_completed');
286
- expect(event).not.toBeNull();
287
- });
288
- });
@@ -1,157 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
- import { createHash } from 'crypto';
3
- import express from 'express';
4
- import request from 'supertest';
5
- import { Pool } from 'pg';
6
- import { createServerModule } from '../../src/server/index.js';
7
- import type { ServerModuleAdapter, OrgRole } from '../../src/server/types.js';
8
-
9
- const describeWithDb = process.env.DATABASE_URL ? describe : describe.skip;
10
-
11
- function sha256(s: string): string {
12
- return createHash('sha256').update(s).digest('hex');
13
- }
14
-
15
- let currentUserId: number | null = null;
16
- let currentOrgId: number | null = null;
17
-
18
- const testAdapter: ServerModuleAdapter = {
19
- getCurrentUserId: async () => currentUserId,
20
- getOrganizationIdForUser: async () => currentOrgId,
21
- isUserOrgAdmin: async (userId) => userId <= 2,
22
- logger: { info: () => {}, warn: () => {}, error: () => {} },
23
- getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
24
- getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
25
- findUserByEmail: async (email) => {
26
- const m = email.match(/^u(\d+)@test\.com$/);
27
- return m ? { id: parseInt(m[1]), email } : null;
28
- },
29
- createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
30
- setUserPassword: async () => {},
31
- hashPassword: async (p) => `h:${p}`,
32
- verifyPassword: async (p, h) => h === `h:${p}`,
33
- invalidateAllUserSessions: async () => {},
34
- sendInviteEmail: async () => {},
35
- sendOwnershipTransferEmail: async () => {},
36
- sendEmailChangeVerification: async () => {},
37
- sendEmailChangeOldNotice: async () => {},
38
- sendEmailChangedFinalNotice: async () => {},
39
- sendPasswordResetEmail: async () => {},
40
- sendOrgDeletionNotice: async () => {},
41
- emitNotification: async () => {},
42
- };
43
-
44
- async function cleanAll(pool: Pool) {
45
- await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
46
- tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
47
- tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
48
- }
49
-
50
- async function seedOrg(pool: Pool, orgId = 1) {
51
- await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
52
- VALUES (${orgId}, 'Test Org ${orgId}', 'test-org-${orgId}', 1, '{}') ON CONFLICT DO NOTHING`);
53
- await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role) VALUES
54
- (${orgId}, 1, 'owner'), (${orgId}, 2, 'admin'), (${orgId}, 3, 'member'), (${orgId}, 4, 'viewer')
55
- ON CONFLICT DO NOTHING`);
56
- }
57
-
58
- describeWithDb('cascade preview', () => {
59
- let pool: Pool;
60
- let app: express.Express;
61
-
62
- beforeAll(async () => {
63
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
64
- const mod = createServerModule({ adapter: testAdapter, pool, features: {} });
65
- app = express();
66
- app.use(express.json());
67
- app.use(mod.router);
68
- });
69
-
70
- afterAll(async () => {
71
- await pool.end();
72
- });
73
-
74
- beforeEach(async () => {
75
- await cleanAll(pool);
76
- currentUserId = 1;
77
- currentOrgId = 1;
78
- await seedOrg(pool);
79
- });
80
-
81
- it('returns cascade preview for member who has sent invitations', async () => {
82
- // Member 2 sends an invitation — insert directly with correct column names
83
- await pool.query(
84
- `INSERT INTO tm_invitations (org_id, invited_by_user_id, email, role, token_hash, code_encrypted, expires_at)
85
- VALUES (1, 2, 'invited@example.com', 'member', $1, 'fake-enc-abc123', NOW() + INTERVAL '7 days')`,
86
- [sha256('tok-abc123')]
87
- );
88
-
89
- currentUserId = 1; // admin calling on behalf
90
-
91
- const res = await request(app)
92
- .get('/orgs/1/members/2/cascade-preview');
93
-
94
- expect(res.status).toBeLessThan(500);
95
- expect(res.body).toBeDefined();
96
-
97
- // Must include the membership being removed
98
- const body = res.body;
99
- expect(body).toHaveProperty('membership');
100
-
101
- // Must include pending invitations they sent
102
- expect(body).toHaveProperty('pendingInvitations');
103
- const invitations = body.pendingInvitations as Array<{ invited_by_user_id?: number; invitedByUserId?: number }>;
104
- expect(Array.isArray(invitations)).toBe(true);
105
- expect(invitations.length).toBeGreaterThanOrEqual(1);
106
- });
107
-
108
- it('returns cascade preview for member who has NOT sent invitations', async () => {
109
- // Member 4 has no invitations
110
- const res = await request(app)
111
- .get('/orgs/1/members/4/cascade-preview');
112
-
113
- expect(res.status).toBeLessThan(500);
114
- expect(res.body).toBeDefined();
115
-
116
- const body = res.body;
117
- expect(body).toHaveProperty('membership');
118
- expect(body).toHaveProperty('pendingInvitations');
119
-
120
- const invitations = body.pendingInvitations as unknown[];
121
- expect(Array.isArray(invitations)).toBe(true);
122
- expect(invitations.length).toBe(0);
123
- });
124
-
125
- it('returns 404 for non-existent member', async () => {
126
- const res = await request(app)
127
- .get('/orgs/1/members/999/cascade-preview');
128
-
129
- expect(res.status).toBe(404);
130
- });
131
-
132
- it('returns 403 for non-admin caller', async () => {
133
- currentUserId = 3; // member, not admin
134
- const res = await request(app)
135
- .get('/orgs/1/members/4/cascade-preview');
136
-
137
- expect(res.status).toBe(403);
138
- });
139
-
140
- it('preview for member with multiple pending invitations lists all of them', async () => {
141
- await pool.query(
142
- `INSERT INTO tm_invitations (org_id, invited_by_user_id, email, role, token_hash, code_encrypted, expires_at)
143
- VALUES
144
- (1, 2, 'a@example.com', 'member', $1, 'fake-enc-001', NOW() + INTERVAL '7 days'),
145
- (1, 2, 'b@example.com', 'viewer', $2, 'fake-enc-002', NOW() + INTERVAL '7 days'),
146
- (1, 2, 'c@example.com', 'admin', $3, 'fake-enc-003', NOW() + INTERVAL '7 days')`,
147
- [sha256('tok-001'), sha256('tok-002'), sha256('tok-003')]
148
- );
149
-
150
- const res = await request(app)
151
- .get('/orgs/1/members/2/cascade-preview');
152
-
153
- expect(res.status).toBeLessThan(500);
154
- const invitations = res.body.pendingInvitations as unknown[];
155
- expect(invitations.length).toBe(3);
156
- });
157
- });