@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,104 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { resetPassword } from '../api.js';
3
-
4
- interface PasswordResetPageProps {
5
- token: string;
6
- }
7
-
8
- type State = 'idle' | 'submitting' | 'success' | 'error';
9
-
10
- export function PasswordResetPage({ token }: PasswordResetPageProps) {
11
- const [newPassword, setNewPassword] = useState('');
12
- const [confirmPassword, setConfirmPassword] = useState('');
13
- const [state, setState] = useState<State>('idle');
14
- const [error, setError] = useState<string | null>(null);
15
-
16
- const passwordsMatch = newPassword === confirmPassword;
17
- const isValid = newPassword.length >= 8 && passwordsMatch;
18
-
19
- async function handleSubmit(e: React.FormEvent) {
20
- e.preventDefault();
21
- if (!isValid) return;
22
- setState('submitting');
23
- setError(null);
24
- try {
25
- await resetPassword(token, newPassword);
26
- setState('success');
27
- } catch (err) {
28
- setError(err instanceof Error ? err.message : 'Failed to reset password');
29
- setState('error');
30
- }
31
- }
32
-
33
- if (state === 'success') {
34
- return (
35
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
36
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
37
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
38
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
39
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
40
- </svg>
41
- </div>
42
- <h1 className="text-xl font-bold text-slate-900 mb-2">Password reset!</h1>
43
- <p className="text-sm text-slate-600 mb-6">Your password has been successfully updated.</p>
44
- <a href="/login" 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">
45
- Log In
46
- </a>
47
- </div>
48
- </div>
49
- );
50
- }
51
-
52
- const submitting = state === 'submitting';
53
-
54
- return (
55
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
56
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8">
57
- <h1 className="text-xl font-bold text-slate-900 mb-2">Reset Password</h1>
58
- <p className="text-sm text-slate-600 mb-6">Enter and confirm your new password.</p>
59
- <form onSubmit={handleSubmit} className="space-y-4">
60
- <div>
61
- <label className="block text-sm font-medium text-slate-700 mb-1">New Password</label>
62
- <input
63
- type="password"
64
- value={newPassword}
65
- onChange={(e) => setNewPassword(e.target.value)}
66
- required
67
- minLength={8}
68
- disabled={submitting}
69
- placeholder="At least 8 characters"
70
- 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"
71
- />
72
- </div>
73
- <div>
74
- <label className="block text-sm font-medium text-slate-700 mb-1">Confirm Password</label>
75
- <input
76
- type="password"
77
- value={confirmPassword}
78
- onChange={(e) => setConfirmPassword(e.target.value)}
79
- required
80
- disabled={submitting}
81
- placeholder="Repeat your password"
82
- className={`w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-1 disabled:bg-slate-50 ${
83
- confirmPassword && !passwordsMatch
84
- ? 'border-red-400 focus:border-red-500 focus:ring-red-500'
85
- : 'border-slate-300 focus:border-blue-500 focus:ring-blue-500'
86
- }`}
87
- />
88
- {confirmPassword && !passwordsMatch && (
89
- <p className="mt-1 text-xs text-red-600">Passwords do not match</p>
90
- )}
91
- </div>
92
- {error && <p className="text-sm text-red-600">{error}</p>}
93
- <button
94
- type="submit"
95
- disabled={!isValid || submitting}
96
- 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
97
- >
98
- {submitting ? 'Resetting…' : 'Reset Password'}
99
- </button>
100
- </form>
101
- </div>
102
- </div>
103
- );
104
- }
@@ -1,71 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { requestPasswordReset } from '../api.js';
3
-
4
- export function PasswordResetRequestPage() {
5
- const [email, setEmail] = useState('');
6
- const [submitting, setSubmitting] = useState(false);
7
- const [submitted, setSubmitted] = useState(false);
8
-
9
- async function handleSubmit(e: React.FormEvent) {
10
- e.preventDefault();
11
- setSubmitting(true);
12
- try {
13
- await requestPasswordReset(email.trim());
14
- } catch {
15
- // Intentionally swallow — always show success to prevent enumeration
16
- } finally {
17
- setSubmitting(false);
18
- setSubmitted(true);
19
- }
20
- }
21
-
22
- if (submitted) {
23
- return (
24
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
25
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8 text-center">
26
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
27
- <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
28
- <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" />
29
- </svg>
30
- </div>
31
- <h1 className="text-xl font-bold text-slate-900 mb-2">Check your inbox</h1>
32
- <p className="text-sm text-slate-600">
33
- If an account exists for <strong>{email}</strong>, you will receive a password reset link shortly.
34
- </p>
35
- </div>
36
- </div>
37
- );
38
- }
39
-
40
- return (
41
- <div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
42
- <div className="bg-white rounded-xl shadow-md w-full max-w-md p-8">
43
- <h1 className="text-xl font-bold text-slate-900 mb-2">Forgot Password</h1>
44
- <p className="text-sm text-slate-600 mb-6">
45
- Enter your email address and we will send you a link to reset your password.
46
- </p>
47
- <form onSubmit={handleSubmit} className="space-y-4">
48
- <div>
49
- <label className="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
50
- <input
51
- type="email"
52
- value={email}
53
- onChange={(e) => setEmail(e.target.value)}
54
- required
55
- disabled={submitting}
56
- placeholder="you@company.com"
57
- 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"
58
- />
59
- </div>
60
- <button
61
- type="submit"
62
- disabled={submitting || !email.trim()}
63
- 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
64
- >
65
- {submitting ? 'Sending…' : 'Send Reset Link'}
66
- </button>
67
- </form>
68
- </div>
69
- </div>
70
- );
71
- }
@@ -1,20 +0,0 @@
1
- import React from 'react';
2
- import { PlaceholderCard } from '../components/PlaceholderCard.js';
3
-
4
- /**
5
- * PlaceholderPage — full-page wrapper for the PlaceholderCard.
6
- * Host products mount this at their /team route.
7
- */
8
- export function PlaceholderPage(): React.ReactElement {
9
- return (
10
- <div style={{
11
- minHeight: '100vh',
12
- display: 'flex',
13
- alignItems: 'center',
14
- justifyContent: 'center',
15
- backgroundColor: '#f8fafc',
16
- }}>
17
- <PlaceholderCard />
18
- </div>
19
- );
20
- }
@@ -1,401 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- adminListOrgs,
4
- adminGetOrg,
5
- adminRestoreOrg,
6
- adminAppointOwner,
7
- adminHardDeleteOrg,
8
- adminAddMember,
9
- adminRemoveMember,
10
- adminLockUser,
11
- adminUnlockUser,
12
- adminResetPassword,
13
- } from '../api.js';
14
- import type { SuperAdminOrgSummary, PublicMember, OrgRole } from '../types.js';
15
- import { RoleBadge } from '../components/RoleBadge.js';
16
-
17
- interface ActionModalState {
18
- type:
19
- | 'restore'
20
- | 'appoint_owner'
21
- | 'hard_delete'
22
- | 'add_member'
23
- | 'remove_member'
24
- | 'lock_user'
25
- | 'unlock_user'
26
- | 'reset_password'
27
- | null;
28
- orgId?: number;
29
- userId?: number;
30
- }
31
-
32
- export function SuperAdminDashboard() {
33
- const [orgs, setOrgs] = useState<SuperAdminOrgSummary[]>([]);
34
- const [loading, setLoading] = useState(true);
35
- const [error, setError] = useState<string | null>(null);
36
- const [search, setSearch] = useState('');
37
- const [expandedOrg, setExpandedOrg] = useState<number | null>(null);
38
- const [orgDetail, setOrgDetail] = useState<(SuperAdminOrgSummary & { members: PublicMember[] }) | null>(null);
39
- const [modal, setModal] = useState<ActionModalState>({ type: null });
40
- const [reason, setReason] = useState('');
41
- const [targetUserId, setTargetUserId] = useState('');
42
- const [addRole, setAddRole] = useState<OrgRole>('member');
43
- const [submitting, setSubmitting] = useState(false);
44
- const [actionError, setActionError] = useState<string | null>(null);
45
- const [actionSuccess, setActionSuccess] = useState<string | null>(null);
46
-
47
- useEffect(() => {
48
- adminListOrgs()
49
- .then(setOrgs)
50
- .catch((e: Error) => setError(e.message))
51
- .finally(() => setLoading(false));
52
- }, []);
53
-
54
- async function expandOrg(id: number) {
55
- if (expandedOrg === id) { setExpandedOrg(null); setOrgDetail(null); return; }
56
- setExpandedOrg(id);
57
- setOrgDetail(null);
58
- try {
59
- const detail = await adminGetOrg(id);
60
- setOrgDetail(detail);
61
- } catch (e) {
62
- console.error(e);
63
- }
64
- }
65
-
66
- function openModal(type: ActionModalState['type'], orgId?: number, userId?: number) {
67
- setModal({ type, orgId, userId });
68
- setReason('');
69
- setTargetUserId('');
70
- setAddRole('member');
71
- setActionError(null);
72
- setActionSuccess(null);
73
- }
74
-
75
- async function handleAction() {
76
- if (!modal.type) return;
77
- setSubmitting(true);
78
- setActionError(null);
79
- try {
80
- const uid = parseInt(targetUserId, 10);
81
- switch (modal.type) {
82
- case 'restore':
83
- await adminRestoreOrg(modal.orgId!);
84
- setActionSuccess('Organization restored.');
85
- break;
86
- case 'appoint_owner':
87
- await adminAppointOwner(modal.orgId!, uid, reason);
88
- setActionSuccess('Owner appointed.');
89
- break;
90
- case 'hard_delete':
91
- await adminHardDeleteOrg(modal.orgId!, reason);
92
- setActionSuccess('Organization hard-deleted.');
93
- break;
94
- case 'add_member':
95
- await adminAddMember(modal.orgId!, uid, addRole, reason);
96
- setActionSuccess('Member added.');
97
- break;
98
- case 'remove_member':
99
- await adminRemoveMember(modal.orgId!, modal.userId ?? uid, reason);
100
- setActionSuccess('Member removed.');
101
- break;
102
- case 'lock_user':
103
- await adminLockUser(modal.userId ?? uid, reason);
104
- setActionSuccess('User locked.');
105
- break;
106
- case 'unlock_user':
107
- await adminUnlockUser(modal.userId ?? uid, reason);
108
- setActionSuccess('User unlocked.');
109
- break;
110
- case 'reset_password':
111
- await adminResetPassword(modal.userId ?? uid, reason);
112
- setActionSuccess('Password reset email sent.');
113
- break;
114
- }
115
- // Refresh org list
116
- const updated = await adminListOrgs();
117
- setOrgs(updated);
118
- if (modal.orgId && expandedOrg === modal.orgId) {
119
- const detail = await adminGetOrg(modal.orgId);
120
- setOrgDetail(detail);
121
- }
122
- } catch (err) {
123
- setActionError(err instanceof Error ? err.message : 'Action failed');
124
- } finally {
125
- setSubmitting(false);
126
- }
127
- }
128
-
129
- const filtered = orgs.filter(
130
- (o) =>
131
- o.name.toLowerCase().includes(search.toLowerCase()) ||
132
- o.slug.toLowerCase().includes(search.toLowerCase()) ||
133
- o.owner_email.toLowerCase().includes(search.toLowerCase())
134
- );
135
-
136
- const modalTitles: Record<NonNullable<ActionModalState['type']>, string> = {
137
- restore: 'Restore Organization',
138
- appoint_owner: 'Appoint New Owner',
139
- hard_delete: 'Hard Delete Organization',
140
- add_member: 'Add Member',
141
- remove_member: 'Remove Member',
142
- lock_user: 'Lock User',
143
- unlock_user: 'Unlock User',
144
- reset_password: 'Reset User Password',
145
- };
146
-
147
- const needsTargetUser = (type: ActionModalState['type']) =>
148
- ['appoint_owner', 'add_member', 'lock_user', 'unlock_user', 'reset_password'].includes(type ?? '');
149
-
150
- return (
151
- <div className="max-w-6xl mx-auto px-4 py-8">
152
- <div className="flex items-center gap-3 mb-6">
153
- <span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800 border border-purple-300">
154
- SUPER ADMIN
155
- </span>
156
- <h1 className="text-2xl font-bold text-slate-900">Admin Dashboard</h1>
157
- </div>
158
-
159
- {error && (
160
- <div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700 mb-6">
161
- {error}
162
- </div>
163
- )}
164
-
165
- {/* Search */}
166
- <div className="mb-4">
167
- <input
168
- type="text"
169
- value={search}
170
- onChange={(e) => setSearch(e.target.value)}
171
- placeholder="Search orgs by name, slug, or owner email…"
172
- 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"
173
- />
174
- </div>
175
-
176
- {loading ? (
177
- <div className="flex items-center justify-center py-16">
178
- <div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
179
- </div>
180
- ) : (
181
- <div className="space-y-2">
182
- {filtered.map((org) => (
183
- <div key={org.id} className="rounded-lg border border-slate-200 shadow-sm overflow-hidden">
184
- {/* Org header row */}
185
- <div className="flex items-center gap-4 px-4 py-3 bg-white hover:bg-slate-50 transition-colors">
186
- <button
187
- onClick={() => expandOrg(org.id)}
188
- className="flex-1 flex items-center gap-4 text-left"
189
- >
190
- <div className="flex-1">
191
- <div className="flex items-center gap-2">
192
- <span className="text-sm font-semibold text-slate-900">{org.name}</span>
193
- <span className="text-xs text-slate-400 font-mono">/{org.slug}</span>
194
- {org.deleted_at && (
195
- <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">
196
- Deleted
197
- </span>
198
- )}
199
- </div>
200
- <div className="flex items-center gap-4 mt-0.5 text-xs text-slate-500">
201
- <span>Owner: {org.owner_email}</span>
202
- <span>{org.member_count} members</span>
203
- <span>Created {new Date(org.created_at).toLocaleDateString()}</span>
204
- </div>
205
- </div>
206
- <svg
207
- className={`h-4 w-4 text-slate-400 transition-transform ${expandedOrg === org.id ? 'rotate-180' : ''}`}
208
- fill="none" viewBox="0 0 24 24" stroke="currentColor"
209
- >
210
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
211
- </svg>
212
- </button>
213
-
214
- {/* Quick actions */}
215
- <div className="flex gap-2 shrink-0">
216
- {org.deleted_at ? (
217
- <button
218
- onClick={() => openModal('restore', org.id)}
219
- className="text-xs text-green-600 hover:text-green-800 font-medium border border-green-300 rounded px-2 py-1 hover:bg-green-50 transition-colors"
220
- >
221
- Restore
222
- </button>
223
- ) : (
224
- <>
225
- <button
226
- onClick={() => openModal('appoint_owner', org.id)}
227
- className="text-xs text-blue-600 hover:text-blue-800 font-medium border border-blue-300 rounded px-2 py-1 hover:bg-blue-50 transition-colors"
228
- >
229
- Appoint Owner
230
- </button>
231
- <button
232
- onClick={() => openModal('add_member', org.id)}
233
- className="text-xs text-slate-600 hover:text-slate-800 font-medium border border-slate-300 rounded px-2 py-1 hover:bg-slate-50 transition-colors"
234
- >
235
- Add Member
236
- </button>
237
- <button
238
- onClick={() => openModal('hard_delete', org.id)}
239
- className="text-xs text-red-600 hover:text-red-800 font-medium border border-red-300 rounded px-2 py-1 hover:bg-red-50 transition-colors"
240
- >
241
- Hard Delete
242
- </button>
243
- </>
244
- )}
245
- </div>
246
- </div>
247
-
248
- {/* Expanded member list */}
249
- {expandedOrg === org.id && (
250
- <div className="border-t border-slate-100 bg-slate-50 px-4 py-3">
251
- {!orgDetail ? (
252
- <div className="flex items-center gap-2 text-sm text-slate-500">
253
- <div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-400 border-t-transparent" />
254
- Loading members…
255
- </div>
256
- ) : (
257
- <table className="w-full text-sm">
258
- <thead>
259
- <tr className="text-xs text-slate-500 border-b border-slate-200">
260
- <th className="pb-2 text-left font-medium">Member</th>
261
- <th className="pb-2 text-left font-medium">Role</th>
262
- <th className="pb-2 text-left font-medium">Joined</th>
263
- <th className="pb-2 text-right font-medium">Actions</th>
264
- </tr>
265
- </thead>
266
- <tbody>
267
- {orgDetail.members.map((m) => (
268
- <tr key={m.id} className="border-b border-slate-100 last:border-0">
269
- <td className="py-2 pr-4">
270
- <div>
271
- <span className="font-medium text-slate-800">{m.name ?? m.email}</span>
272
- {m.name && <span className="ml-2 text-xs text-slate-400">{m.email}</span>}
273
- </div>
274
- </td>
275
- <td className="py-2 pr-4"><RoleBadge role={m.role} /></td>
276
- <td className="py-2 pr-4 text-xs text-slate-500">
277
- {new Date(m.joined_at).toLocaleDateString()}
278
- </td>
279
- <td className="py-2 text-right">
280
- <div className="flex justify-end gap-2">
281
- <button
282
- onClick={() => openModal('lock_user', org.id, m.user_id)}
283
- className="text-xs text-amber-600 hover:text-amber-800 font-medium"
284
- >
285
- Lock
286
- </button>
287
- <button
288
- onClick={() => openModal('unlock_user', org.id, m.user_id)}
289
- className="text-xs text-green-600 hover:text-green-800 font-medium"
290
- >
291
- Unlock
292
- </button>
293
- <button
294
- onClick={() => openModal('reset_password', org.id, m.user_id)}
295
- className="text-xs text-blue-600 hover:text-blue-800 font-medium"
296
- >
297
- Reset PW
298
- </button>
299
- <button
300
- onClick={() => openModal('remove_member', org.id, m.user_id)}
301
- className="text-xs text-red-600 hover:text-red-800 font-medium"
302
- >
303
- Remove
304
- </button>
305
- </div>
306
- </td>
307
- </tr>
308
- ))}
309
- </tbody>
310
- </table>
311
- )}
312
- </div>
313
- )}
314
- </div>
315
- ))}
316
- {filtered.length === 0 && (
317
- <p className="text-center text-sm text-slate-500 py-8">No organizations found.</p>
318
- )}
319
- </div>
320
- )}
321
-
322
- {/* Action Modal */}
323
- {modal.type && (
324
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
325
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-md p-6">
326
- <h2 className="text-lg font-semibold text-slate-900 mb-4">
327
- {modalTitles[modal.type]}
328
- </h2>
329
-
330
- {needsTargetUser(modal.type) && !modal.userId && (
331
- <div className="mb-4">
332
- <label className="block text-sm font-medium text-slate-700 mb-1">Target User ID</label>
333
- <input
334
- type="number"
335
- value={targetUserId}
336
- onChange={(e) => setTargetUserId(e.target.value)}
337
- placeholder="User ID"
338
- 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"
339
- />
340
- </div>
341
- )}
342
-
343
- {modal.type === 'add_member' && (
344
- <div className="mb-4">
345
- <label className="block text-sm font-medium text-slate-700 mb-1">Role</label>
346
- <select
347
- value={addRole}
348
- onChange={(e) => setAddRole(e.target.value as OrgRole)}
349
- 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"
350
- >
351
- <option value="admin">Admin</option>
352
- <option value="member">Member</option>
353
- <option value="viewer">Viewer</option>
354
- </select>
355
- </div>
356
- )}
357
-
358
- <div className="mb-4">
359
- <label className="block text-sm font-medium text-slate-700 mb-1">
360
- Reason <span className="text-red-500">*</span>
361
- </label>
362
- <textarea
363
- value={reason}
364
- onChange={(e) => setReason(e.target.value)}
365
- rows={3}
366
- placeholder="Provide a reason for this action (required for audit log)"
367
- 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 resize-none"
368
- />
369
- </div>
370
-
371
- {actionError && <p className="mb-3 text-sm text-red-600">{actionError}</p>}
372
- {actionSuccess && <p className="mb-3 text-sm text-green-600">{actionSuccess}</p>}
373
-
374
- <div className="flex gap-3 justify-end">
375
- <button
376
- onClick={() => setModal({ type: null })}
377
- disabled={submitting}
378
- className="rounded-md border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50 transition-colors"
379
- >
380
- {actionSuccess ? 'Close' : 'Cancel'}
381
- </button>
382
- {!actionSuccess && (
383
- <button
384
- onClick={handleAction}
385
- disabled={submitting || !reason.trim()}
386
- className={`rounded-md px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
387
- modal.type === 'hard_delete' || modal.type === 'remove_member' || modal.type === 'lock_user'
388
- ? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
389
- : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'
390
- }`}
391
- >
392
- {submitting ? 'Processing…' : 'Confirm'}
393
- </button>
394
- )}
395
- </div>
396
- </div>
397
- </div>
398
- )}
399
- </div>
400
- );
401
- }
@@ -1,78 +0,0 @@
1
- export type OrgRole = 'owner' | 'admin' | 'member' | 'viewer';
2
- export type TransferStatus = 'pending' | 'accepted' | 'cancelled' | 'expired';
3
-
4
- export interface PublicOrg {
5
- id: number;
6
- name: string;
7
- slug: string;
8
- settings: Record<string, unknown>;
9
- created_at: string;
10
- }
11
-
12
- export interface PublicMember {
13
- id: number;
14
- user_id: number;
15
- org_id: number;
16
- role: OrgRole;
17
- email: string;
18
- name?: string;
19
- joined_at: string;
20
- removed_at?: string;
21
- removed_by_user_id?: number;
22
- removal_reason?: string;
23
- }
24
-
25
- export interface PendingInvitation {
26
- id: number;
27
- org_id: number;
28
- email: string;
29
- role: OrgRole;
30
- invited_by_user_id: number;
31
- expires_at: string;
32
- resent_count: number;
33
- created_at: string;
34
- code?: string; // decrypted, only returned to admins on explicit request
35
- }
36
-
37
- export interface AuditEvent {
38
- id: string;
39
- org_id: number | null;
40
- actor_user_id: number | null;
41
- actor_display_name: string; // 'Varshyl Support' for super_admin entries
42
- actor_type: 'user' | 'super_admin';
43
- action: string;
44
- target_type: string | null;
45
- target_id: string | null;
46
- before_state: Record<string, unknown> | null;
47
- after_state: Record<string, unknown> | null;
48
- created_at: string;
49
- }
50
-
51
- export interface OwnershipTransfer {
52
- id: number;
53
- org_id: number;
54
- from_user_id: number;
55
- to_user_id: number;
56
- status: TransferStatus;
57
- initiated_at: string;
58
- expires_at: string;
59
- }
60
-
61
- export interface CurrentMembership {
62
- org: PublicOrg;
63
- user_id: number;
64
- role: OrgRole;
65
- joined_at: string;
66
- }
67
-
68
- export interface SuperAdminOrgSummary {
69
- id: number;
70
- name: string;
71
- slug: string;
72
- member_count: number;
73
- owner_email: string;
74
- deleted_at: string | null;
75
- created_at: string;
76
- }
77
-
78
- export type ApiError = { error: string; details?: string[] };