@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.
- package/dist/client/actions.d.ts +20 -0
- package/dist/client/actions.d.ts.map +1 -0
- package/dist/client/actions.js +23 -0
- package/dist/client/actions.js.map +1 -0
- package/dist/client/api.d.ts +74 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +245 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/components/AddMemberForm.d.ts +12 -0
- package/dist/client/components/AddMemberForm.d.ts.map +1 -0
- package/dist/client/components/AddMemberForm.js +37 -0
- package/dist/client/components/AddMemberForm.js.map +1 -0
- package/dist/client/components/AuditEventRow.d.ts +7 -0
- package/dist/client/components/AuditEventRow.d.ts.map +1 -0
- package/dist/client/components/AuditEventRow.js +20 -0
- package/dist/client/components/AuditEventRow.js.map +1 -0
- package/dist/client/components/CascadePreview.d.ts +11 -0
- package/dist/client/components/CascadePreview.d.ts.map +1 -0
- package/dist/client/components/CascadePreview.js +7 -0
- package/dist/client/components/CascadePreview.js.map +1 -0
- package/dist/client/components/DangerZoneCard.d.ts +10 -0
- package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
- package/dist/client/components/DangerZoneCard.js +28 -0
- package/dist/client/components/DangerZoneCard.js.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.js +26 -0
- package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
- package/dist/client/components/InviteForm.d.ts +7 -0
- package/dist/client/components/InviteForm.d.ts.map +1 -0
- package/dist/client/components/InviteForm.js +35 -0
- package/dist/client/components/InviteForm.js.map +1 -0
- package/dist/client/components/MemberRow.d.ts +10 -0
- package/dist/client/components/MemberRow.d.ts.map +1 -0
- package/dist/client/components/MemberRow.js +17 -0
- package/dist/client/components/MemberRow.js.map +1 -0
- package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
- package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
- package/dist/client/components/OrgPeopleRoster.js +13 -0
- package/dist/client/components/OrgPeopleRoster.js.map +1 -0
- package/dist/client/components/PendingTransferBanner.d.ts +10 -0
- package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
- package/dist/client/components/PendingTransferBanner.js +48 -0
- package/dist/client/components/PendingTransferBanner.js.map +1 -0
- package/dist/client/components/PlaceholderCard.d.ts +7 -0
- package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
- package/dist/client/components/PlaceholderCard.js +15 -0
- package/dist/client/components/PlaceholderCard.js.map +1 -0
- package/dist/client/components/RoleBadge.d.ts +5 -0
- package/dist/client/components/RoleBadge.d.ts.map +1 -0
- package/dist/client/components/RoleBadge.js +17 -0
- package/dist/client/components/RoleBadge.js.map +1 -0
- package/dist/client/components/RoleSelect.d.ts +10 -0
- package/dist/client/components/RoleSelect.d.ts.map +1 -0
- package/dist/client/components/RoleSelect.js +12 -0
- package/dist/client/components/RoleSelect.js.map +1 -0
- package/dist/client/components/SeatUsagePanel.d.ts +6 -0
- package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
- package/dist/client/components/SeatUsagePanel.js +13 -0
- package/dist/client/components/SeatUsagePanel.js.map +1 -0
- package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
- package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
- package/dist/client/hooks/useCurrentMembership.js +20 -0
- package/dist/client/hooks/useCurrentMembership.js.map +1 -0
- package/dist/client/hooks/useMembers.d.ts +10 -0
- package/dist/client/hooks/useMembers.d.ts.map +1 -0
- package/dist/client/hooks/useMembers.js +20 -0
- package/dist/client/hooks/useMembers.js.map +1 -0
- package/dist/client/hooks/useOrgMembers.d.ts +20 -0
- package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
- package/dist/client/hooks/useOrgMembers.js +63 -0
- package/dist/client/hooks/useOrgMembers.js.map +1 -0
- package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
- package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
- package/dist/client/hooks/usePendingInvitations.js +20 -0
- package/dist/client/hooks/usePendingInvitations.js.map +1 -0
- package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
- package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
- package/dist/client/hooks/usePendingTransfer.js +23 -0
- package/dist/client/hooks/usePendingTransfer.js.map +1 -0
- package/dist/client/index.d.ts +31 -0
- package/dist/client/index.d.ts.map +1 -0
- package/{src/client/index.ts → dist/client/index.js} +6 -54
- package/dist/client/index.js.map +1 -0
- package/dist/client/pages/AuditLogPage.d.ts +6 -0
- package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
- package/dist/client/pages/AuditLogPage.js +51 -0
- package/dist/client/pages/AuditLogPage.js.map +1 -0
- package/dist/client/pages/EmailChangePage.d.ts +8 -0
- package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
- package/dist/client/pages/EmailChangePage.js +52 -0
- package/dist/client/pages/EmailChangePage.js.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.js +42 -0
- package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
- package/dist/client/pages/InvitationCodePage.d.ts +6 -0
- package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
- package/dist/client/pages/InvitationCodePage.js +28 -0
- package/dist/client/pages/InvitationCodePage.js.map +1 -0
- package/dist/client/pages/MembersPage.d.ts +6 -0
- package/dist/client/pages/MembersPage.d.ts.map +1 -0
- package/dist/client/pages/MembersPage.js +67 -0
- package/dist/client/pages/MembersPage.js.map +1 -0
- package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
- package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
- package/dist/client/pages/OrgPeoplePage.js +40 -0
- package/dist/client/pages/OrgPeoplePage.js.map +1 -0
- package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
- package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
- package/dist/client/pages/OrgSettingsPage.js +78 -0
- package/dist/client/pages/OrgSettingsPage.js.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.js +68 -0
- package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
- package/dist/client/pages/PasswordResetPage.d.ts +6 -0
- package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetPage.js +34 -0
- package/dist/client/pages/PasswordResetPage.js.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.js +27 -0
- package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
- package/dist/client/pages/PlaceholderPage.d.ts +7 -0
- package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
- package/dist/client/pages/PlaceholderPage.js +16 -0
- package/dist/client/pages/PlaceholderPage.js.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.js +123 -0
- package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
- package/dist/client/theme.d.ts +12 -0
- package/dist/client/theme.d.ts.map +1 -0
- package/dist/client/theme.js +16 -0
- package/dist/client/theme.js.map +1 -0
- package/dist/client/types.d.ts +78 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/org-admin.d.ts +45 -0
- package/dist/server/org-admin.d.ts.map +1 -0
- package/dist/server/org-admin.js +63 -0
- package/dist/server/org-admin.js.map +1 -0
- package/dist/server/routes/orgs.routes.d.ts.map +1 -1
- package/dist/server/routes/orgs.routes.js +81 -12
- package/dist/server/routes/orgs.routes.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +18 -11
- package/.eslintrc.cjs +0 -18
- package/CHANGELOG.md +0 -159
- package/src/client/api.ts +0 -314
- package/src/client/components/AuditEventRow.tsx +0 -59
- package/src/client/components/CascadePreview.tsx +0 -36
- package/src/client/components/DangerZoneCard.tsx +0 -103
- package/src/client/components/InvitationCodeDisplay.tsx +0 -48
- package/src/client/components/InviteForm.tsx +0 -77
- package/src/client/components/MemberRow.tsx +0 -69
- package/src/client/components/PendingTransferBanner.tsx +0 -98
- package/src/client/components/PlaceholderCard.tsx +0 -26
- package/src/client/components/RoleBadge.tsx +0 -26
- package/src/client/components/RoleSelect.tsx +0 -35
- package/src/client/hooks/.gitkeep +0 -0
- package/src/client/hooks/useCurrentMembership.ts +0 -24
- package/src/client/hooks/useMembers.ts +0 -24
- package/src/client/hooks/usePendingInvitations.ts +0 -24
- package/src/client/hooks/usePendingTransfer.ts +0 -27
- package/src/client/pages/AuditLogPage.tsx +0 -164
- package/src/client/pages/EmailChangePage.tsx +0 -144
- package/src/client/pages/InvitationAcceptPage.tsx +0 -163
- package/src/client/pages/InvitationCodePage.tsx +0 -108
- package/src/client/pages/MembersPage.tsx +0 -290
- package/src/client/pages/OrgSettingsPage.tsx +0 -185
- package/src/client/pages/OwnershipTransferPage.tsx +0 -163
- package/src/client/pages/PasswordResetPage.tsx +0 -104
- package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
- package/src/client/pages/PlaceholderPage.tsx +0 -20
- package/src/client/pages/SuperAdminDashboard.tsx +0 -401
- package/src/client/types.ts +0 -78
- package/src/index.ts +0 -24
- package/src/server/crypto.ts +0 -47
- package/src/server/index.ts +0 -167
- package/src/server/middleware/require-membership.ts +0 -48
- package/src/server/middleware/require-role.ts +0 -19
- package/src/server/middleware/require-super-admin.ts +0 -32
- package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
- package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
- package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
- package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
- package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
- package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
- package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
- package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
- package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
- package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
- package/src/server/migrations/0011_seed_super_admin.sql +0 -15
- package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
- package/src/server/routes/admin.routes.ts +0 -208
- package/src/server/routes/audit.routes.ts +0 -93
- package/src/server/routes/health.routes.ts +0 -46
- package/src/server/routes/invitations.routes.ts +0 -252
- package/src/server/routes/me.routes.ts +0 -143
- package/src/server/routes/orgs.routes.ts +0 -428
- package/src/server/routes/transfer.routes.ts +0 -110
- package/src/server/services/.gitkeep +0 -0
- package/src/server/services/audit.service.ts +0 -49
- package/src/server/services/email-change.service.ts +0 -178
- package/src/server/services/invitations.service.ts +0 -316
- package/src/server/services/memberships.service.ts +0 -129
- package/src/server/services/organizations.service.ts +0 -110
- package/src/server/services/ownership.service.ts +0 -170
- package/src/server/services/password-reset.service.ts +0 -94
- package/src/server/services/super-admin.service.ts +0 -321
- package/src/server/sql/.gitkeep +0 -0
- package/src/server/types.ts +0 -145
- package/src/shared/types.ts +0 -24
- package/tests/integration/audit-fires.test.ts +0 -288
- package/tests/integration/cascade-preview.test.ts +0 -157
- package/tests/integration/email-change.test.ts +0 -190
- package/tests/integration/feature-flags.test.ts +0 -213
- package/tests/integration/invitations-code.test.ts +0 -218
- package/tests/integration/invitations-expiry.test.ts +0 -216
- package/tests/integration/invitations-resend.test.ts +0 -241
- package/tests/integration/invitations-revoke.test.ts +0 -226
- package/tests/integration/invitations-switch-org.test.ts +0 -156
- package/tests/integration/invitations-token.test.ts +0 -221
- package/tests/integration/migrations.test.ts +0 -119
- package/tests/integration/only-owner-protections.test.ts +0 -130
- package/tests/integration/org-lifecycle.test.ts +0 -169
- package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
- package/tests/integration/ownership-transfer-expire.test.ts +0 -171
- package/tests/integration/ownership-transfer-happy.test.ts +0 -184
- package/tests/integration/ownership-transfer-locks.test.ts +0 -146
- package/tests/integration/password-reset.test.ts +0 -200
- package/tests/integration/super-admin-actions.test.ts +0 -180
- package/tests/integration/super-admin-restrictions.test.ts +0 -209
- package/tests/setup/global-setup.ts +0 -20
- package/tests/unit/adapter-shape.test.ts +0 -330
- package/tests/unit/role-permissions.test.ts +0 -236
- package/tests/unit/validation.test.ts +0 -304
- package/tsconfig.client.json +0 -13
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- 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
|
-
}
|
package/src/client/types.ts
DELETED
|
@@ -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[] };
|