@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,164 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { useCurrentMembership } from '../hooks/useCurrentMembership.js';
3
- import { AuditEventRow } from '../components/AuditEventRow.js';
4
- import { getAuditLog } from '../api.js';
5
- import type { AuditEvent } from '../types.js';
6
-
7
- interface AuditLogPageProps {
8
- orgId: number;
9
- }
10
-
11
- const PAGE_SIZE = 25;
12
-
13
- export function AuditLogPage({ orgId }: AuditLogPageProps) {
14
- const { membership } = useCurrentMembership();
15
- const [events, setEvents] = useState<AuditEvent[]>([]);
16
- const [total, setTotal] = useState(0);
17
- const [page, setPage] = useState(1);
18
- const [loading, setLoading] = useState(false);
19
- const [error, setError] = useState<string | null>(null);
20
- const [actionFilter, setActionFilter] = useState('');
21
- const [initialized, setInitialized] = useState(false);
22
-
23
- const role = membership?.role;
24
- const canView = role === 'owner' || role === 'admin';
25
-
26
- async function load(p: number, action: string) {
27
- setLoading(true);
28
- setError(null);
29
- try {
30
- const result = await getAuditLog(orgId, { page: p, limit: PAGE_SIZE, action: action || undefined });
31
- setEvents(result.events);
32
- setTotal(result.total);
33
- setPage(result.page);
34
- } catch (err) {
35
- setError(err instanceof Error ? err.message : 'Failed to load audit log');
36
- } finally {
37
- setLoading(false);
38
- setInitialized(true);
39
- }
40
- }
41
-
42
- // Load on first render after membership check
43
- React.useEffect(() => {
44
- if (canView && !initialized) {
45
- load(1, actionFilter);
46
- }
47
- }, [canView]);
48
-
49
- async function handleFilter(e: React.FormEvent) {
50
- e.preventDefault();
51
- await load(1, actionFilter);
52
- }
53
-
54
- const totalPages = Math.ceil(total / PAGE_SIZE);
55
-
56
- if (!canView) {
57
- return (
58
- <div className="max-w-4xl mx-auto px-4 py-8">
59
- <div className="rounded-md bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-700">
60
- You do not have permission to view the audit log.
61
- </div>
62
- </div>
63
- );
64
- }
65
-
66
- return (
67
- <div className="max-w-5xl mx-auto px-4 py-8">
68
- <div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-6">
69
- <h1 className="text-2xl font-bold text-slate-900 flex-1">Audit Log</h1>
70
- <form onSubmit={handleFilter} className="flex gap-2">
71
- <input
72
- type="text"
73
- value={actionFilter}
74
- onChange={(e) => setActionFilter(e.target.value)}
75
- placeholder="Filter by action…"
76
- className="rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
77
- />
78
- <button
79
- type="submit"
80
- className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
81
- >
82
- Filter
83
- </button>
84
- {actionFilter && (
85
- <button
86
- type="button"
87
- onClick={() => { setActionFilter(''); load(1, ''); }}
88
- className="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-600 hover:bg-slate-50 transition-colors"
89
- >
90
- Clear
91
- </button>
92
- )}
93
- </form>
94
- </div>
95
-
96
- {error && (
97
- <div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700 mb-4">
98
- {error}
99
- </div>
100
- )}
101
-
102
- {loading ? (
103
- <div className="flex items-center justify-center py-16">
104
- <div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
105
- </div>
106
- ) : (
107
- <>
108
- <div className="overflow-hidden rounded-lg border border-slate-200 shadow-sm">
109
- <table className="w-full text-left">
110
- <thead className="bg-slate-50 border-b border-slate-200">
111
- <tr>
112
- <th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Actor</th>
113
- <th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
114
- <th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Target</th>
115
- <th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">When</th>
116
- </tr>
117
- </thead>
118
- <tbody className="bg-white divide-y divide-slate-100">
119
- {events.map((event) => (
120
- <AuditEventRow key={event.id} event={event} />
121
- ))}
122
- {events.length === 0 && initialized && (
123
- <tr>
124
- <td colSpan={4} className="py-10 text-center text-sm text-slate-500">
125
- No audit events found.
126
- </td>
127
- </tr>
128
- )}
129
- </tbody>
130
- </table>
131
- </div>
132
-
133
- {/* Pagination */}
134
- {totalPages > 1 && (
135
- <div className="mt-4 flex items-center justify-between">
136
- <p className="text-sm text-slate-600">
137
- Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, total)} of {total} events
138
- </p>
139
- <div className="flex gap-2">
140
- <button
141
- onClick={() => load(page - 1, actionFilter)}
142
- disabled={page <= 1 || loading}
143
- className="rounded-md border border-slate-300 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
144
- >
145
- Previous
146
- </button>
147
- <span className="flex items-center px-2 text-sm text-slate-600">
148
- {page} / {totalPages}
149
- </span>
150
- <button
151
- onClick={() => load(page + 1, actionFilter)}
152
- disabled={page >= totalPages || loading}
153
- className="rounded-md border border-slate-300 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
154
- >
155
- Next
156
- </button>
157
- </div>
158
- </div>
159
- )}
160
- </>
161
- )}
162
- </div>
163
- );
164
- }
@@ -1,144 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { requestEmailChange, verifyEmailChange, cancelEmailChange } from '../api.js';
3
-
4
- interface EmailChangePageProps {
5
- /** 'request' | 'verify' | 'cancel' */
6
- mode: 'request' | 'verify' | 'cancel';
7
- token?: string;
8
- }
9
-
10
- export function EmailChangePage({ mode, token }: EmailChangePageProps) {
11
- const [newEmail, setNewEmail] = useState('');
12
- const [submitting, setSubmitting] = useState(false);
13
- const [error, setError] = useState<string | null>(null);
14
- const [success, setSuccess] = useState(false);
15
-
16
- // Auto-submit verify/cancel on mount
17
- useEffect(() => {
18
- if ((mode === 'verify' || mode === 'cancel') && token) {
19
- setSubmitting(true);
20
- const fn = mode === 'verify' ? verifyEmailChange : cancelEmailChange;
21
- fn(token)
22
- .then(() => setSuccess(true))
23
- .catch((e: Error) => setError(e.message))
24
- .finally(() => setSubmitting(false));
25
- }
26
- }, [mode, token]);
27
-
28
- if (mode === 'verify' || mode === 'cancel') {
29
- if (submitting) {
30
- return (
31
- <div className="min-h-screen flex items-center justify-center bg-slate-50">
32
- <div className="text-center">
33
- <div className="mx-auto h-10 w-10 animate-spin rounded-full border-2 border-blue-500 border-t-transparent mb-4" />
34
- <p className="text-sm text-slate-600">
35
- {mode === 'verify' ? 'Verifying your email change…' : 'Cancelling email change…'}
36
- </p>
37
- </div>
38
- </div>
39
- );
40
- }
41
- if (success) {
42
- return (
43
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
44
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
45
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
46
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
48
- </svg>
49
- </div>
50
- <h1 className="text-xl font-bold text-slate-900 mb-2">
51
- {mode === 'verify' ? 'Email updated!' : 'Email change cancelled.'}
52
- </h1>
53
- <p className="text-sm text-slate-600 mb-6">
54
- {mode === 'verify'
55
- ? 'Your email address has been successfully updated.'
56
- : 'The email change request has been cancelled. Your email remains unchanged.'}
57
- </p>
58
- <a href="/settings" className="inline-block rounded-md border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors">
59
- Back to Settings
60
- </a>
61
- </div>
62
- </div>
63
- );
64
- }
65
- return (
66
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
67
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
68
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
69
- <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
70
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
71
- </svg>
72
- </div>
73
- <h1 className="text-xl font-bold text-slate-900 mb-2">Something went wrong</h1>
74
- <p className="text-sm text-slate-600 mb-6">{error}</p>
75
- <a href="/settings" className="inline-block rounded-md border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors">
76
- Back to Settings
77
- </a>
78
- </div>
79
- </div>
80
- );
81
- }
82
-
83
- // mode === 'request'
84
- async function handleSubmit(e: React.FormEvent) {
85
- e.preventDefault();
86
- setSubmitting(true);
87
- setError(null);
88
- try {
89
- await requestEmailChange(newEmail.trim());
90
- setSuccess(true);
91
- } catch (err) {
92
- setError(err instanceof Error ? err.message : 'Failed to request email change');
93
- } finally {
94
- setSubmitting(false);
95
- }
96
- }
97
-
98
- if (success) {
99
- return (
100
- <div className="max-w-lg mx-auto px-4 py-8 text-center">
101
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
102
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
103
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
104
- </svg>
105
- </div>
106
- <h1 className="text-xl font-bold text-slate-900 mb-2">Check your email</h1>
107
- <p className="text-sm text-slate-600">
108
- We sent a confirmation link to <strong>{newEmail}</strong>. Click the link to confirm your new email address.
109
- </p>
110
- </div>
111
- );
112
- }
113
-
114
- return (
115
- <div className="max-w-lg mx-auto px-4 py-8">
116
- <h1 className="text-2xl font-bold text-slate-900 mb-2">Change Email Address</h1>
117
- <p className="text-sm text-slate-600 mb-6">
118
- Enter your new email address. We will send a confirmation link to verify the change.
119
- </p>
120
- <form onSubmit={handleSubmit} className="space-y-4">
121
- <div>
122
- <label className="block text-sm font-medium text-slate-700 mb-1">New Email Address</label>
123
- <input
124
- type="email"
125
- value={newEmail}
126
- onChange={(e) => setNewEmail(e.target.value)}
127
- required
128
- disabled={submitting}
129
- placeholder="new@email.com"
130
- className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-slate-50"
131
- />
132
- </div>
133
- {error && <p className="text-sm text-red-600">{error}</p>}
134
- <button
135
- type="submit"
136
- disabled={submitting || !newEmail.trim()}
137
- className="rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 transition-colors"
138
- >
139
- {submitting ? 'Sending…' : 'Send Confirmation'}
140
- </button>
141
- </form>
142
- </div>
143
- );
144
- }
@@ -1,163 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { acceptInvitationByToken } from '../api.js';
3
- import type { OrgRole } from '../types.js';
4
-
5
- interface InvitationAcceptPageProps {
6
- /** The magic-link token, typically extracted from the URL query string */
7
- token: string;
8
- /** Whether the current user is authenticated */
9
- isAuthenticated: boolean;
10
- loginUrl?: string;
11
- signupUrl?: string;
12
- }
13
-
14
- type State =
15
- | { status: 'loading' }
16
- | { status: 'success'; orgId: number; role: OrgRole }
17
- | { status: 'already_member'; message: string }
18
- | { status: 'error'; message: string }
19
- | { status: 'unauthenticated' };
20
-
21
- export function InvitationAcceptPage({
22
- token,
23
- isAuthenticated,
24
- loginUrl = '/login',
25
- signupUrl = '/signup',
26
- }: InvitationAcceptPageProps) {
27
- const [state, setState] = useState<State>(
28
- isAuthenticated ? { status: 'loading' } : { status: 'unauthenticated' }
29
- );
30
-
31
- useEffect(() => {
32
- if (!isAuthenticated || !token) return;
33
-
34
- acceptInvitationByToken(token)
35
- .then(({ orgId, role }) => setState({ status: 'success', orgId, role }))
36
- .catch((err: Error) => {
37
- const msg = err.message ?? 'Something went wrong';
38
- if (msg.includes('already') || msg.includes('member')) {
39
- setState({ status: 'already_member', message: msg });
40
- } else {
41
- setState({ status: 'error', message: msg });
42
- }
43
- });
44
- }, [token, isAuthenticated]);
45
-
46
- if (state.status === 'unauthenticated') {
47
- return (
48
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
49
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
50
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
51
- <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
52
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
53
- </svg>
54
- </div>
55
- <h1 className="text-xl font-bold text-slate-900 mb-2">You have been invited!</h1>
56
- <p className="text-sm text-slate-600 mb-6">
57
- Please log in or create an account to accept this invitation.
58
- </p>
59
- <div className="flex flex-col gap-3">
60
- <a
61
- href={`${loginUrl}?redirect_token=${encodeURIComponent(token)}`}
62
- className="rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 text-center transition-colors"
63
- >
64
- Log In
65
- </a>
66
- <a
67
- href={`${signupUrl}?redirect_token=${encodeURIComponent(token)}`}
68
- className="rounded-md border border-slate-300 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 text-center transition-colors"
69
- >
70
- Create Account
71
- </a>
72
- </div>
73
- </div>
74
- </div>
75
- );
76
- }
77
-
78
- if (state.status === 'loading') {
79
- return (
80
- <div className="min-h-screen flex items-center justify-center bg-slate-50">
81
- <div className="text-center">
82
- <div className="mx-auto h-10 w-10 animate-spin rounded-full border-2 border-blue-500 border-t-transparent mb-4" />
83
- <p className="text-sm text-slate-600">Accepting your invitation…</p>
84
- </div>
85
- </div>
86
- );
87
- }
88
-
89
- if (state.status === 'success') {
90
- const roleLabels: Record<OrgRole, string> = {
91
- owner: 'Owner',
92
- admin: 'Admin',
93
- member: 'Member',
94
- viewer: 'Viewer',
95
- };
96
- return (
97
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
98
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
99
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
100
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
101
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
102
- </svg>
103
- </div>
104
- <h1 className="text-xl font-bold text-slate-900 mb-2">Welcome aboard!</h1>
105
- <p className="text-sm text-slate-600 mb-6">
106
- You have joined the organization as <strong>{roleLabels[state.role]}</strong>.
107
- </p>
108
- <a
109
- href={`/orgs/${state.orgId}`}
110
- className="inline-block rounded-md bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
111
- >
112
- Go to Organization
113
- </a>
114
- </div>
115
- </div>
116
- );
117
- }
118
-
119
- if (state.status === 'already_member') {
120
- return (
121
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
122
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
123
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-amber-100">
124
- <svg className="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
125
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
126
- </svg>
127
- </div>
128
- <h1 className="text-xl font-bold text-slate-900 mb-2">Already a member</h1>
129
- <p className="text-sm text-slate-600 mb-6">{state.message}</p>
130
- <a
131
- href="/"
132
- className="inline-block rounded-md border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors"
133
- >
134
- Back to Home
135
- </a>
136
- </div>
137
- </div>
138
- );
139
- }
140
-
141
- // error state
142
- return (
143
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
144
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
145
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
146
- <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
147
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
148
- </svg>
149
- </div>
150
- <h1 className="text-xl font-bold text-slate-900 mb-2">Invitation failed</h1>
151
- <p className="text-sm text-slate-600 mb-6">
152
- {'message' in state ? state.message : 'This invitation link is invalid or has expired.'}
153
- </p>
154
- <a
155
- href="/"
156
- className="inline-block rounded-md border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors"
157
- >
158
- Back to Home
159
- </a>
160
- </div>
161
- </div>
162
- );
163
- }
@@ -1,108 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { acceptInvitationByCode } from '../api.js';
3
- import type { OrgRole } from '../types.js';
4
-
5
- interface InvitationCodePageProps {
6
- prefillEmail?: string;
7
- }
8
-
9
- type State =
10
- | { status: 'idle' }
11
- | { status: 'submitting' }
12
- | { status: 'success'; orgId: number; role: OrgRole }
13
- | { status: 'error'; message: string };
14
-
15
- export function InvitationCodePage({ prefillEmail = '' }: InvitationCodePageProps) {
16
- const [email, setEmail] = useState(prefillEmail);
17
- const [code, setCode] = useState('');
18
- const [state, setState] = useState<State>({ status: 'idle' });
19
-
20
- async function handleSubmit(e: React.FormEvent) {
21
- e.preventDefault();
22
- setState({ status: 'submitting' });
23
- try {
24
- const result = await acceptInvitationByCode(email.trim(), code.trim());
25
- setState({ status: 'success', ...result });
26
- } catch (err) {
27
- setState({ status: 'error', message: err instanceof Error ? err.message : 'Invalid code' });
28
- }
29
- }
30
-
31
- if (state.status === 'success') {
32
- const roleLabels: Record<OrgRole, string> = {
33
- owner: 'Owner', admin: 'Admin', member: 'Member', viewer: 'Viewer',
34
- };
35
- return (
36
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
37
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
38
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
39
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
40
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
41
- </svg>
42
- </div>
43
- <h1 className="text-xl font-bold text-slate-900 mb-2">You're in!</h1>
44
- <p className="text-sm text-slate-600 mb-6">
45
- You joined as <strong>{roleLabels[state.role]}</strong>.
46
- </p>
47
- <a
48
- href={`/orgs/${state.orgId}`}
49
- className="inline-block rounded-md bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
50
- >
51
- Go to Organization
52
- </a>
53
- </div>
54
- </div>
55
- );
56
- }
57
-
58
- const submitting = state.status === 'submitting';
59
-
60
- return (
61
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
62
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8">
63
- <h1 className="text-xl font-bold text-slate-900 mb-2">Enter Invitation Code</h1>
64
- <p className="text-sm text-slate-600 mb-6">
65
- Enter the email address your invitation was sent to and the 6-digit code you received.
66
- </p>
67
- <form onSubmit={handleSubmit} className="space-y-4">
68
- <div>
69
- <label className="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
70
- <input
71
- type="email"
72
- value={email}
73
- onChange={(e) => setEmail(e.target.value)}
74
- required
75
- disabled={submitting}
76
- placeholder="you@company.com"
77
- className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-slate-50"
78
- />
79
- </div>
80
- <div>
81
- <label className="block text-sm font-medium text-slate-700 mb-1">6-Digit Code</label>
82
- <input
83
- type="text"
84
- value={code}
85
- onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
86
- required
87
- disabled={submitting}
88
- placeholder="123456"
89
- maxLength={6}
90
- inputMode="numeric"
91
- className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm font-mono tracking-widest text-center focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-slate-50"
92
- />
93
- </div>
94
- {state.status === 'error' && (
95
- <p className="text-sm text-red-600">{state.message}</p>
96
- )}
97
- <button
98
- type="submit"
99
- disabled={submitting || code.length !== 6 || !email.trim()}
100
- className="w-full rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
101
- >
102
- {submitting ? 'Verifying…' : 'Accept Invitation'}
103
- </button>
104
- </form>
105
- </div>
106
- </div>
107
- );
108
- }