@qwickapps/server 1.5.1 → 1.6.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/CHANGELOG.md +43 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +41 -0
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/guards.d.ts.map +1 -1
- package/dist/core/guards.js +77 -0
- package/dist/core/guards.js.map +1 -1
- package/dist/core/health-manager.d.ts +4 -0
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +6 -1
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +55 -5
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +57 -19
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
- package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
- package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
- package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
- package/dist/plugins/api-keys/index.d.ts +14 -0
- package/dist/plugins/api-keys/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/index.js +17 -0
- package/dist/plugins/api-keys/index.js.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
- package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
- package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/index.js +7 -0
- package/dist/plugins/api-keys/middleware/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/index.d.ts +7 -0
- package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/index.js +7 -0
- package/dist/plugins/api-keys/stores/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
- package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
- package/dist/plugins/api-keys/types.d.ts +268 -0
- package/dist/plugins/api-keys/types.d.ts.map +1 -0
- package/dist/plugins/api-keys/types.js +56 -0
- package/dist/plugins/api-keys/types.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -1
- package/dist/plugins/auth/auth-plugin.js +17 -1
- package/dist/plugins/auth/auth-plugin.js.map +1 -1
- package/dist/plugins/auth/auth-plugin.test.js +133 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +6 -2
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +10 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/devices/__tests__/token-utils.test.js +4 -2
- package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +21 -4
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/qwickbrain/index.d.ts +25 -0
- package/dist/plugins/qwickbrain/index.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/index.js +24 -0
- package/dist/plugins/qwickbrain/index.js.map +1 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts +23 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.js +528 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -0
- package/dist/plugins/qwickbrain/types.d.ts +131 -0
- package/dist/plugins/qwickbrain/types.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/types.js +9 -0
- package/dist/plugins/qwickbrain/types.js.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +59 -1
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +22 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist-ui/assets/index-5nX8fM1a.js +469 -0
- package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +68 -0
- package/dist-ui-lib/components/index.d.ts +2 -1
- package/dist-ui-lib/index.js +2642 -2281
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
- package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
- package/package.json +3 -2
- package/src/core/control-panel.ts +47 -0
- package/src/core/guards.ts +89 -0
- package/src/core/health-manager.ts +6 -1
- package/src/core/plugin-registry.ts +123 -25
- package/src/core/types.ts +2 -0
- package/src/index.ts +11 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
- package/src/plugins/api-keys/index.ts +49 -0
- package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
- package/src/plugins/api-keys/middleware/index.ts +12 -0
- package/src/plugins/api-keys/stores/index.ts +7 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
- package/src/plugins/api-keys/types.ts +243 -0
- package/src/plugins/auth/auth-plugin.test.ts +167 -0
- package/src/plugins/auth/auth-plugin.ts +17 -1
- package/src/plugins/auth/env-config.ts +6 -2
- package/src/plugins/auth/types.ts +10 -0
- package/src/plugins/devices/__tests__/token-utils.test.ts +4 -2
- package/src/plugins/frontend-app-plugin.ts +24 -4
- package/src/plugins/index.ts +15 -0
- package/src/plugins/qwickbrain/index.ts +33 -0
- package/src/plugins/qwickbrain/qwickbrain-plugin.ts +642 -0
- package/src/plugins/qwickbrain/types.ts +146 -0
- package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/stores/postgres-store.ts +69 -0
- package/src/plugins/users/types.ts +25 -0
- package/ui/src/App.tsx +6 -1
- package/ui/src/api/controlPanelApi.ts +206 -37
- package/ui/src/components/index.ts +6 -0
- package/ui/src/pages/APIKeysPage.tsx +661 -0
- package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
- package/ui/src/pages/UsersPage.tsx +225 -2
- package/dist-ui/assets/index-CynOqPkb.js +0 -469
- package/dist-ui/assets/index-CynOqPkb.js.map +0 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AcceptInvitationPage Component
|
|
3
|
+
*
|
|
4
|
+
* Standalone page for users to accept invitations and activate their accounts.
|
|
5
|
+
* Can be used in control panel or frontend applications.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect } from 'react';
|
|
11
|
+
import { Box, Card, CardContent, Alert, CircularProgress } from '@mui/material';
|
|
12
|
+
import { Text, Button } from '@qwickapps/react-framework';
|
|
13
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
14
|
+
import ErrorIcon from '@mui/icons-material/Error';
|
|
15
|
+
import { api, type User } from '../api/controlPanelApi';
|
|
16
|
+
|
|
17
|
+
export interface AcceptInvitationPageProps {
|
|
18
|
+
/** Invitation token (if not provided, will extract from URL) */
|
|
19
|
+
token?: string;
|
|
20
|
+
/** Title text */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Subtitle text */
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
/** Success message */
|
|
25
|
+
successMessage?: string;
|
|
26
|
+
/** URL to redirect to after successful activation (optional) */
|
|
27
|
+
redirectUrl?: string;
|
|
28
|
+
/** Label for the redirect button */
|
|
29
|
+
redirectLabel?: string;
|
|
30
|
+
/** Callback when invitation is accepted successfully */
|
|
31
|
+
onSuccess?: (user: User) => void;
|
|
32
|
+
/** Callback when invitation acceptance fails */
|
|
33
|
+
onError?: (error: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AcceptInvitationPage({
|
|
37
|
+
token: tokenProp,
|
|
38
|
+
title = 'Accept Invitation',
|
|
39
|
+
subtitle = 'Activate your account',
|
|
40
|
+
successMessage = 'Your account has been activated successfully!',
|
|
41
|
+
redirectUrl,
|
|
42
|
+
redirectLabel = 'Go to App',
|
|
43
|
+
onSuccess,
|
|
44
|
+
onError,
|
|
45
|
+
}: AcceptInvitationPageProps) {
|
|
46
|
+
const [loading, setLoading] = useState(true);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [success, setSuccess] = useState(false);
|
|
49
|
+
const [user, setUser] = useState<User | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const acceptInvitation = async () => {
|
|
53
|
+
// Get token from prop or URL query parameter
|
|
54
|
+
let inviteToken = tokenProp;
|
|
55
|
+
if (!inviteToken) {
|
|
56
|
+
const params = new URLSearchParams(window.location.search);
|
|
57
|
+
inviteToken = params.get('token') || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!inviteToken) {
|
|
61
|
+
setError('No invitation token provided');
|
|
62
|
+
setLoading(false);
|
|
63
|
+
onError?.('No invitation token provided');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await api.acceptInvitation(inviteToken);
|
|
69
|
+
setUser(result.user);
|
|
70
|
+
setSuccess(true);
|
|
71
|
+
onSuccess?.(result.user);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to accept invitation';
|
|
74
|
+
setError(errorMessage);
|
|
75
|
+
onError?.(errorMessage);
|
|
76
|
+
} finally {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
acceptInvitation();
|
|
82
|
+
}, [tokenProp, onSuccess, onError]);
|
|
83
|
+
|
|
84
|
+
const handleRedirect = () => {
|
|
85
|
+
if (redirectUrl) {
|
|
86
|
+
window.location.href = redirectUrl;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Box
|
|
92
|
+
sx={{
|
|
93
|
+
minHeight: '100vh',
|
|
94
|
+
display: 'flex',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
bgcolor: 'var(--theme-background)',
|
|
98
|
+
p: 3,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<Card sx={{ maxWidth: 500, width: '100%', bgcolor: 'var(--theme-surface)' }}>
|
|
102
|
+
<CardContent sx={{ p: 4 }}>
|
|
103
|
+
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
|
104
|
+
<Text variant="h4" content={title} customColor="var(--theme-text-primary)" style={{ marginBottom: '8px' }} />
|
|
105
|
+
<Text variant="body2" content={subtitle} customColor="var(--theme-text-secondary)" />
|
|
106
|
+
</Box>
|
|
107
|
+
|
|
108
|
+
{loading && (
|
|
109
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, py: 4 }}>
|
|
110
|
+
<CircularProgress />
|
|
111
|
+
<Text variant="body2" content="Activating your account..." customColor="var(--theme-text-secondary)" />
|
|
112
|
+
</Box>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{error && !loading && (
|
|
116
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
|
|
117
|
+
<ErrorIcon sx={{ fontSize: 64, color: 'var(--theme-error)' }} />
|
|
118
|
+
<Alert severity="error" sx={{ width: '100%' }}>
|
|
119
|
+
{error}
|
|
120
|
+
</Alert>
|
|
121
|
+
<Text
|
|
122
|
+
variant="body2"
|
|
123
|
+
content="The invitation may have expired or is invalid. Please contact support."
|
|
124
|
+
customColor="var(--theme-text-secondary)"
|
|
125
|
+
style={{ textAlign: 'center' }}
|
|
126
|
+
/>
|
|
127
|
+
</Box>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{success && !loading && (
|
|
131
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
|
|
132
|
+
<CheckCircleIcon sx={{ fontSize: 64, color: 'var(--theme-success)' }} />
|
|
133
|
+
<Alert severity="success" sx={{ width: '100%' }}>
|
|
134
|
+
{successMessage}
|
|
135
|
+
</Alert>
|
|
136
|
+
|
|
137
|
+
{user && (
|
|
138
|
+
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
|
139
|
+
<Text
|
|
140
|
+
variant="body1"
|
|
141
|
+
content={`Welcome, ${user.name || user.email}!`}
|
|
142
|
+
customColor="var(--theme-text-primary)"
|
|
143
|
+
fontWeight="500"
|
|
144
|
+
style={{ marginBottom: '4px' }}
|
|
145
|
+
/>
|
|
146
|
+
<Text
|
|
147
|
+
variant="body2"
|
|
148
|
+
content="Your account is now active and ready to use."
|
|
149
|
+
customColor="var(--theme-text-secondary)"
|
|
150
|
+
/>
|
|
151
|
+
</Box>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{redirectUrl && (
|
|
155
|
+
<Button
|
|
156
|
+
variant="primary"
|
|
157
|
+
label={redirectLabel}
|
|
158
|
+
icon="arrow_forward"
|
|
159
|
+
onClick={handleRedirect}
|
|
160
|
+
fullWidth
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
</Box>
|
|
164
|
+
)}
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
</Box>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -38,6 +38,7 @@ import LocalOfferIcon from '@mui/icons-material/LocalOffer';
|
|
|
38
38
|
import BlockIcon from '@mui/icons-material/Block';
|
|
39
39
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
40
40
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
41
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
41
42
|
import {
|
|
42
43
|
api,
|
|
43
44
|
type User,
|
|
@@ -93,6 +94,10 @@ export function UsersPage({
|
|
|
93
94
|
const [bans, setBans] = useState<Ban[]>([]);
|
|
94
95
|
const [bansTotal, setBansTotal] = useState(0);
|
|
95
96
|
|
|
97
|
+
// Invitations state
|
|
98
|
+
const [invitations, setInvitations] = useState<User[]>([]);
|
|
99
|
+
const [invitationsTotal, setInvitationsTotal] = useState(0);
|
|
100
|
+
|
|
96
101
|
// Shared state
|
|
97
102
|
const [loading, setLoading] = useState(true);
|
|
98
103
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -106,6 +111,16 @@ export function UsersPage({
|
|
|
106
111
|
expiresAt: '',
|
|
107
112
|
});
|
|
108
113
|
|
|
114
|
+
// Invite dialog state
|
|
115
|
+
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
|
116
|
+
const [newInvite, setNewInvite] = useState({
|
|
117
|
+
email: '',
|
|
118
|
+
name: '',
|
|
119
|
+
role: '',
|
|
120
|
+
expiresInDays: 7,
|
|
121
|
+
});
|
|
122
|
+
const [inviteResult, setInviteResult] = useState<{ token: string; inviteLink: string } | null>(null);
|
|
123
|
+
|
|
109
124
|
// Entitlements lookup state
|
|
110
125
|
const [entitlementsDialogOpen, setEntitlementsDialogOpen] = useState(false);
|
|
111
126
|
const [entitlementsSearch, setEntitlementsSearch] = useState('');
|
|
@@ -192,6 +207,23 @@ export function UsersPage({
|
|
|
192
207
|
}
|
|
193
208
|
}, [features.bans]);
|
|
194
209
|
|
|
210
|
+
// Fetch invitations
|
|
211
|
+
const fetchInvitations = useCallback(async () => {
|
|
212
|
+
if (!features.users) return;
|
|
213
|
+
|
|
214
|
+
setLoading(true);
|
|
215
|
+
try {
|
|
216
|
+
const data = await api.getInvitations();
|
|
217
|
+
setInvitations(data.users || []);
|
|
218
|
+
setInvitationsTotal(data.total);
|
|
219
|
+
setError(null);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch invitations');
|
|
222
|
+
} finally {
|
|
223
|
+
setLoading(false);
|
|
224
|
+
}
|
|
225
|
+
}, [features.users]);
|
|
226
|
+
|
|
195
227
|
// Initial fetch and tab-based fetching
|
|
196
228
|
useEffect(() => {
|
|
197
229
|
if (!featuresLoaded) return;
|
|
@@ -200,8 +232,10 @@ export function UsersPage({
|
|
|
200
232
|
fetchUsers();
|
|
201
233
|
} else if (activeTab === 1 && features.bans) {
|
|
202
234
|
fetchBans();
|
|
235
|
+
} else if (activeTab === 2 && features.users) {
|
|
236
|
+
fetchInvitations();
|
|
203
237
|
}
|
|
204
|
-
}, [activeTab, featuresLoaded, features.users, features.bans, fetchUsers, fetchBans]);
|
|
238
|
+
}, [activeTab, featuresLoaded, features.users, features.bans, fetchUsers, fetchBans, fetchInvitations]);
|
|
205
239
|
|
|
206
240
|
// Fetch bans count for stats (only on initial load)
|
|
207
241
|
useEffect(() => {
|
|
@@ -248,6 +282,36 @@ export function UsersPage({
|
|
|
248
282
|
}
|
|
249
283
|
};
|
|
250
284
|
|
|
285
|
+
// Invite handlers
|
|
286
|
+
const handleInviteUser = async () => {
|
|
287
|
+
try {
|
|
288
|
+
const result = await api.inviteUser({
|
|
289
|
+
email: newInvite.email,
|
|
290
|
+
name: newInvite.name || undefined,
|
|
291
|
+
role: newInvite.role || undefined,
|
|
292
|
+
expiresInDays: newInvite.expiresInDays,
|
|
293
|
+
});
|
|
294
|
+
setInviteResult({ token: result.token, inviteLink: result.inviteLink });
|
|
295
|
+
setSuccess('User invitation created successfully');
|
|
296
|
+
fetchUsers();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
setError(err instanceof Error ? err.message : 'Failed to invite user');
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleCopyInviteLink = () => {
|
|
303
|
+
if (inviteResult) {
|
|
304
|
+
navigator.clipboard.writeText(inviteResult.inviteLink);
|
|
305
|
+
setSuccess('Invite link copied to clipboard');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const handleCloseInviteDialog = () => {
|
|
310
|
+
setInviteDialogOpen(false);
|
|
311
|
+
setNewInvite({ email: '', name: '', role: '', expiresInDays: 7 });
|
|
312
|
+
setInviteResult(null);
|
|
313
|
+
};
|
|
314
|
+
|
|
251
315
|
// Entitlements handlers
|
|
252
316
|
const handleEntitlementsSearch = async () => {
|
|
253
317
|
if (!entitlementsSearch.trim()) {
|
|
@@ -362,6 +426,7 @@ export function UsersPage({
|
|
|
362
426
|
const tabs: { label: string; count?: number }[] = [];
|
|
363
427
|
if (features.users) tabs.push({ label: 'Users', count: usersTotal });
|
|
364
428
|
if (features.bans) tabs.push({ label: 'Banned', count: bansTotal });
|
|
429
|
+
if (features.users) tabs.push({ label: 'Invitations', count: invitationsTotal });
|
|
365
430
|
|
|
366
431
|
if (!featuresLoaded) {
|
|
367
432
|
return (
|
|
@@ -380,6 +445,14 @@ export function UsersPage({
|
|
|
380
445
|
</Box>
|
|
381
446
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
382
447
|
{headerActions}
|
|
448
|
+
{features.users && (
|
|
449
|
+
<Button
|
|
450
|
+
variant="primary"
|
|
451
|
+
icon="person_add"
|
|
452
|
+
label="Invite User"
|
|
453
|
+
onClick={() => setInviteDialogOpen(true)}
|
|
454
|
+
/>
|
|
455
|
+
)}
|
|
383
456
|
{features.entitlements && (
|
|
384
457
|
<Button
|
|
385
458
|
variant="outlined"
|
|
@@ -390,7 +463,7 @@ export function UsersPage({
|
|
|
390
463
|
)}
|
|
391
464
|
{features.bans && (
|
|
392
465
|
<Button
|
|
393
|
-
variant="
|
|
466
|
+
variant="outlined"
|
|
394
467
|
color="error"
|
|
395
468
|
icon="block"
|
|
396
469
|
label="Ban User"
|
|
@@ -645,9 +718,159 @@ export function UsersPage({
|
|
|
645
718
|
</Table>
|
|
646
719
|
</TableContainer>
|
|
647
720
|
)}
|
|
721
|
+
|
|
722
|
+
{/* Invitations Tab */}
|
|
723
|
+
{activeTab === 2 && features.users && (
|
|
724
|
+
<TableContainer>
|
|
725
|
+
<Table>
|
|
726
|
+
<TableHead>
|
|
727
|
+
<TableRow>
|
|
728
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Email</TableCell>
|
|
729
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Name</TableCell>
|
|
730
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Created</TableCell>
|
|
731
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Expires</TableCell>
|
|
732
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Status</TableCell>
|
|
733
|
+
</TableRow>
|
|
734
|
+
</TableHead>
|
|
735
|
+
<TableBody>
|
|
736
|
+
{invitations.map((invitation) => {
|
|
737
|
+
const isExpired = invitation.invitation_expires_at && new Date(invitation.invitation_expires_at) < new Date();
|
|
738
|
+
return (
|
|
739
|
+
<TableRow key={invitation.id}>
|
|
740
|
+
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
|
|
741
|
+
<Text variant="body1" content={invitation.email} fontWeight="500" />
|
|
742
|
+
</TableCell>
|
|
743
|
+
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
|
|
744
|
+
{invitation.name || '--'}
|
|
745
|
+
</TableCell>
|
|
746
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
747
|
+
{formatDate(invitation.created_at)}
|
|
748
|
+
</TableCell>
|
|
749
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
750
|
+
{formatDate(invitation.invitation_expires_at)}
|
|
751
|
+
</TableCell>
|
|
752
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }}>
|
|
753
|
+
<Chip
|
|
754
|
+
size="small"
|
|
755
|
+
label={isExpired ? 'Expired' : 'Pending'}
|
|
756
|
+
sx={{
|
|
757
|
+
bgcolor: isExpired ? 'var(--theme-error)20' : 'var(--theme-warning)20',
|
|
758
|
+
color: isExpired ? 'var(--theme-error)' : 'var(--theme-warning)',
|
|
759
|
+
}}
|
|
760
|
+
/>
|
|
761
|
+
</TableCell>
|
|
762
|
+
</TableRow>
|
|
763
|
+
);
|
|
764
|
+
})}
|
|
765
|
+
{invitations.length === 0 && !loading && (
|
|
766
|
+
<TableRow>
|
|
767
|
+
<TableCell colSpan={5} align="center" sx={{ py: 4, color: 'var(--theme-text-secondary)' }}>
|
|
768
|
+
No pending invitations
|
|
769
|
+
</TableCell>
|
|
770
|
+
</TableRow>
|
|
771
|
+
)}
|
|
772
|
+
</TableBody>
|
|
773
|
+
</Table>
|
|
774
|
+
</TableContainer>
|
|
775
|
+
)}
|
|
648
776
|
</CardContent>
|
|
649
777
|
</Card>
|
|
650
778
|
|
|
779
|
+
{/* Invite User Dialog */}
|
|
780
|
+
{features.users && (
|
|
781
|
+
<Dialog
|
|
782
|
+
open={inviteDialogOpen}
|
|
783
|
+
onClose={handleCloseInviteDialog}
|
|
784
|
+
maxWidth="sm"
|
|
785
|
+
fullWidth
|
|
786
|
+
>
|
|
787
|
+
<DialogTitle>Invite User</DialogTitle>
|
|
788
|
+
<DialogContent>
|
|
789
|
+
{!inviteResult ? (
|
|
790
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
791
|
+
<TextField
|
|
792
|
+
label="Email"
|
|
793
|
+
fullWidth
|
|
794
|
+
required
|
|
795
|
+
value={newInvite.email}
|
|
796
|
+
onChange={(e) => setNewInvite({ ...newInvite, email: e.target.value })}
|
|
797
|
+
placeholder="user@example.com"
|
|
798
|
+
type="email"
|
|
799
|
+
/>
|
|
800
|
+
<TextField
|
|
801
|
+
label="Name (Optional)"
|
|
802
|
+
fullWidth
|
|
803
|
+
value={newInvite.name}
|
|
804
|
+
onChange={(e) => setNewInvite({ ...newInvite, name: e.target.value })}
|
|
805
|
+
placeholder="Enter user's full name"
|
|
806
|
+
/>
|
|
807
|
+
<TextField
|
|
808
|
+
label="Role (Optional)"
|
|
809
|
+
fullWidth
|
|
810
|
+
value={newInvite.role}
|
|
811
|
+
onChange={(e) => setNewInvite({ ...newInvite, role: e.target.value })}
|
|
812
|
+
placeholder="e.g., admin, editor, viewer"
|
|
813
|
+
helperText="Stored in user metadata for your app to use"
|
|
814
|
+
/>
|
|
815
|
+
<TextField
|
|
816
|
+
label="Invitation Expiry"
|
|
817
|
+
type="number"
|
|
818
|
+
fullWidth
|
|
819
|
+
value={newInvite.expiresInDays}
|
|
820
|
+
onChange={(e) => setNewInvite({ ...newInvite, expiresInDays: parseInt(e.target.value) || 7 })}
|
|
821
|
+
InputProps={{
|
|
822
|
+
endAdornment: <InputAdornment position="end">days</InputAdornment>,
|
|
823
|
+
}}
|
|
824
|
+
helperText="How many days until the invitation expires"
|
|
825
|
+
/>
|
|
826
|
+
</Box>
|
|
827
|
+
) : (
|
|
828
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
829
|
+
<Alert severity="success">
|
|
830
|
+
Invitation created successfully! Share this link with the user:
|
|
831
|
+
</Alert>
|
|
832
|
+
<TextField
|
|
833
|
+
label="Invitation Link"
|
|
834
|
+
fullWidth
|
|
835
|
+
value={inviteResult.inviteLink}
|
|
836
|
+
InputProps={{
|
|
837
|
+
readOnly: true,
|
|
838
|
+
endAdornment: (
|
|
839
|
+
<InputAdornment position="end">
|
|
840
|
+
<Tooltip title="Copy to clipboard">
|
|
841
|
+
<IconButton onClick={handleCopyInviteLink} edge="end">
|
|
842
|
+
<ContentCopyIcon />
|
|
843
|
+
</IconButton>
|
|
844
|
+
</Tooltip>
|
|
845
|
+
</InputAdornment>
|
|
846
|
+
),
|
|
847
|
+
}}
|
|
848
|
+
helperText="Click the icon to copy the link to clipboard"
|
|
849
|
+
/>
|
|
850
|
+
<Alert severity="info">
|
|
851
|
+
The user will need to visit this link to activate their account.
|
|
852
|
+
</Alert>
|
|
853
|
+
</Box>
|
|
854
|
+
)}
|
|
855
|
+
</DialogContent>
|
|
856
|
+
<DialogActions>
|
|
857
|
+
<Button
|
|
858
|
+
variant="text"
|
|
859
|
+
label="Close"
|
|
860
|
+
onClick={handleCloseInviteDialog}
|
|
861
|
+
/>
|
|
862
|
+
{!inviteResult && (
|
|
863
|
+
<Button
|
|
864
|
+
variant="primary"
|
|
865
|
+
label="Create Invitation"
|
|
866
|
+
onClick={handleInviteUser}
|
|
867
|
+
disabled={!newInvite.email}
|
|
868
|
+
/>
|
|
869
|
+
)}
|
|
870
|
+
</DialogActions>
|
|
871
|
+
</Dialog>
|
|
872
|
+
)}
|
|
873
|
+
|
|
651
874
|
{/* Ban User Dialog */}
|
|
652
875
|
{features.bans && (
|
|
653
876
|
<Dialog
|