@checkstack/auth-frontend 0.1.0 → 0.3.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 +155 -0
- package/package.json +1 -1
- package/src/api.ts +2 -2
- package/src/components/ApplicationsTab.tsx +89 -90
- package/src/components/AuthSettingsPage.tsx +87 -100
- package/src/components/LoginPage.tsx +15 -22
- package/src/components/RegisterPage.tsx +17 -28
- package/src/components/RoleDialog.tsx +49 -53
- package/src/components/RolesTab.tsx +60 -47
- package/src/components/StrategiesTab.tsx +90 -96
- package/src/components/TeamAccessEditor.tsx +131 -123
- package/src/components/TeamsTab.tsx +180 -162
- package/src/components/UsersTab.tsx +43 -32
- package/src/hooks/useAccessRules.ts +32 -0
- package/src/hooks/useEnabledStrategies.ts +10 -41
- package/src/index.test.tsx +83 -37
- package/src/index.tsx +29 -51
- package/src/lib/auth-client.ts +17 -22
- package/src/hooks/usePermissions.ts +0 -43
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Card,
|
|
4
4
|
CardHeader,
|
|
@@ -39,8 +39,7 @@ import {
|
|
|
39
39
|
UserPlus,
|
|
40
40
|
UserMinus,
|
|
41
41
|
} from "lucide-react";
|
|
42
|
-
import {
|
|
43
|
-
import { rpcApiRef } from "@checkstack/frontend-api";
|
|
42
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
44
43
|
import { AuthApi } from "@checkstack/auth-common";
|
|
45
44
|
import type { AuthUser } from "../api";
|
|
46
45
|
|
|
@@ -73,45 +72,148 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
73
72
|
canManageTeams,
|
|
74
73
|
onDataChange,
|
|
75
74
|
}) => {
|
|
76
|
-
const
|
|
77
|
-
const authClient = rpcApi.forPlugin(AuthApi);
|
|
75
|
+
const authClient = usePluginClient(AuthApi);
|
|
78
76
|
const toast = useToast();
|
|
79
77
|
|
|
80
|
-
const [teams, setTeams] = useState<Team[]>([]);
|
|
81
|
-
const [loading, setLoading] = useState(true);
|
|
82
78
|
const [teamToDelete, setTeamToDelete] = useState<string>();
|
|
83
79
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
84
80
|
const [editingTeam, setEditingTeam] = useState<Team | undefined>();
|
|
85
81
|
const [membersDialogOpen, setMembersDialogOpen] = useState(false);
|
|
86
|
-
const [
|
|
87
|
-
const [membersLoading, setMembersLoading] = useState(false);
|
|
82
|
+
const [selectedTeamId, setSelectedTeamId] = useState<string>();
|
|
88
83
|
|
|
89
84
|
// Team form state
|
|
90
85
|
const [formName, setFormName] = useState("");
|
|
91
86
|
const [formDescription, setFormDescription] = useState("");
|
|
92
|
-
const [formSaving, setFormSaving] = useState(false);
|
|
93
87
|
|
|
94
88
|
// Member management state
|
|
95
89
|
const [selectedUserId, setSelectedUserId] = useState("");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
|
|
91
|
+
// Query: Teams list
|
|
92
|
+
const {
|
|
93
|
+
data: teams = [],
|
|
94
|
+
isLoading: loading,
|
|
95
|
+
refetch: refetchTeams,
|
|
96
|
+
} = authClient.getTeams.useQuery({}, { enabled: canReadTeams });
|
|
97
|
+
|
|
98
|
+
// Query: Team detail (for members dialog)
|
|
99
|
+
const {
|
|
100
|
+
data: selectedTeamDetail,
|
|
101
|
+
isLoading: membersLoading,
|
|
102
|
+
refetch: refetchTeamDetail,
|
|
103
|
+
} = authClient.getTeam.useQuery(
|
|
104
|
+
{ teamId: selectedTeamId ?? "" },
|
|
105
|
+
{ enabled: !!selectedTeamId && membersDialogOpen }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Mutations
|
|
109
|
+
const createTeamMutation = authClient.createTeam.useMutation({
|
|
110
|
+
onSuccess: () => {
|
|
111
|
+
toast.success("Team created successfully");
|
|
112
|
+
setEditDialogOpen(false);
|
|
113
|
+
void refetchTeams();
|
|
114
|
+
void onDataChange();
|
|
115
|
+
},
|
|
116
|
+
onError: (error) => {
|
|
117
|
+
toast.error(
|
|
118
|
+
error instanceof Error ? error.message : "Failed to create team"
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const updateTeamMutation = authClient.updateTeam.useMutation({
|
|
124
|
+
onSuccess: () => {
|
|
125
|
+
toast.success("Team updated successfully");
|
|
126
|
+
setEditDialogOpen(false);
|
|
127
|
+
void refetchTeams();
|
|
128
|
+
void onDataChange();
|
|
129
|
+
},
|
|
130
|
+
onError: (error) => {
|
|
131
|
+
toast.error(
|
|
132
|
+
error instanceof Error ? error.message : "Failed to update team"
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const deleteTeamMutation = authClient.deleteTeam.useMutation({
|
|
138
|
+
onSuccess: () => {
|
|
139
|
+
toast.success("Team deleted successfully");
|
|
140
|
+
setTeamToDelete(undefined);
|
|
141
|
+
void refetchTeams();
|
|
142
|
+
void onDataChange();
|
|
143
|
+
},
|
|
144
|
+
onError: (error) => {
|
|
145
|
+
toast.error(
|
|
146
|
+
error instanceof Error ? error.message : "Failed to delete team"
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const addMemberMutation = authClient.addUserToTeam.useMutation({
|
|
152
|
+
onSuccess: () => {
|
|
153
|
+
toast.success("Member added successfully");
|
|
154
|
+
setSelectedUserId("");
|
|
155
|
+
void refetchTeamDetail();
|
|
156
|
+
void refetchTeams();
|
|
157
|
+
},
|
|
158
|
+
onError: (error) => {
|
|
159
|
+
toast.error(
|
|
160
|
+
error instanceof Error ? error.message : "Failed to add member"
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const removeMemberMutation = authClient.removeUserFromTeam.useMutation({
|
|
166
|
+
onSuccess: () => {
|
|
167
|
+
toast.success("Member removed");
|
|
168
|
+
void refetchTeamDetail();
|
|
169
|
+
void refetchTeams();
|
|
170
|
+
},
|
|
171
|
+
onError: (error) => {
|
|
172
|
+
toast.error(
|
|
173
|
+
error instanceof Error ? error.message : "Failed to remove member"
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const addManagerMutation = authClient.addTeamManager.useMutation({
|
|
179
|
+
onSuccess: () => {
|
|
180
|
+
toast.success("Member promoted to manager");
|
|
181
|
+
void refetchTeamDetail();
|
|
182
|
+
},
|
|
183
|
+
onError: (error) => {
|
|
184
|
+
toast.error(
|
|
185
|
+
error instanceof Error ? error.message : "Failed to promote to manager"
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const removeManagerMutation = authClient.removeTeamManager.useMutation({
|
|
191
|
+
onSuccess: () => {
|
|
192
|
+
toast.success("Manager role removed");
|
|
193
|
+
void refetchTeamDetail();
|
|
194
|
+
},
|
|
195
|
+
onError: (error) => {
|
|
104
196
|
toast.error(
|
|
105
|
-
error instanceof Error ? error.message : "Failed to
|
|
197
|
+
error instanceof Error ? error.message : "Failed to remove manager role"
|
|
106
198
|
);
|
|
107
|
-
}
|
|
108
|
-
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Reset form when dialog closes
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!editDialogOpen) {
|
|
205
|
+
setEditingTeam(undefined);
|
|
206
|
+
setFormName("");
|
|
207
|
+
setFormDescription("");
|
|
109
208
|
}
|
|
110
|
-
}, [
|
|
209
|
+
}, [editDialogOpen]);
|
|
111
210
|
|
|
211
|
+
// Reset selected team when members dialog closes
|
|
112
212
|
useEffect(() => {
|
|
113
|
-
|
|
114
|
-
|
|
213
|
+
if (!membersDialogOpen) {
|
|
214
|
+
setSelectedTeamId(undefined);
|
|
215
|
+
}
|
|
216
|
+
}, [membersDialogOpen]);
|
|
115
217
|
|
|
116
218
|
const handleCreateTeam = () => {
|
|
117
219
|
setEditingTeam(undefined);
|
|
@@ -127,160 +229,78 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
127
229
|
setEditDialogOpen(true);
|
|
128
230
|
};
|
|
129
231
|
|
|
130
|
-
const handleSaveTeam =
|
|
232
|
+
const handleSaveTeam = () => {
|
|
131
233
|
if (!formName.trim()) {
|
|
132
234
|
toast.error("Team name is required");
|
|
133
235
|
return;
|
|
134
236
|
}
|
|
135
237
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
name: formName,
|
|
148
|
-
description: formDescription || undefined,
|
|
149
|
-
});
|
|
150
|
-
toast.success("Team created successfully");
|
|
151
|
-
}
|
|
152
|
-
setEditDialogOpen(false);
|
|
153
|
-
await loadTeams();
|
|
154
|
-
await onDataChange();
|
|
155
|
-
} catch (error) {
|
|
156
|
-
toast.error(
|
|
157
|
-
error instanceof Error ? error.message : "Failed to save team"
|
|
158
|
-
);
|
|
159
|
-
} finally {
|
|
160
|
-
setFormSaving(false);
|
|
238
|
+
if (editingTeam) {
|
|
239
|
+
updateTeamMutation.mutate({
|
|
240
|
+
id: editingTeam.id,
|
|
241
|
+
name: formName,
|
|
242
|
+
description: formDescription || undefined,
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
createTeamMutation.mutate({
|
|
246
|
+
name: formName,
|
|
247
|
+
description: formDescription || undefined,
|
|
248
|
+
});
|
|
161
249
|
}
|
|
162
250
|
};
|
|
163
251
|
|
|
164
|
-
const handleDeleteTeam =
|
|
252
|
+
const handleDeleteTeam = () => {
|
|
165
253
|
if (!teamToDelete) return;
|
|
166
|
-
|
|
167
|
-
await authClient.deleteTeam(teamToDelete);
|
|
168
|
-
toast.success("Team deleted successfully");
|
|
169
|
-
setTeamToDelete(undefined);
|
|
170
|
-
await loadTeams();
|
|
171
|
-
await onDataChange();
|
|
172
|
-
} catch (error) {
|
|
173
|
-
toast.error(
|
|
174
|
-
error instanceof Error ? error.message : "Failed to delete team"
|
|
175
|
-
);
|
|
176
|
-
}
|
|
254
|
+
deleteTeamMutation.mutate(teamToDelete);
|
|
177
255
|
};
|
|
178
256
|
|
|
179
|
-
const openMembersDialog =
|
|
180
|
-
|
|
257
|
+
const openMembersDialog = (teamId: string) => {
|
|
258
|
+
setSelectedTeamId(teamId);
|
|
181
259
|
setMembersDialogOpen(true);
|
|
182
|
-
try {
|
|
183
|
-
const detail = await authClient.getTeam({ teamId });
|
|
184
|
-
setSelectedTeamDetail(detail ?? undefined);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
toast.error(
|
|
187
|
-
error instanceof Error ? error.message : "Failed to load team details"
|
|
188
|
-
);
|
|
189
|
-
setMembersDialogOpen(false);
|
|
190
|
-
} finally {
|
|
191
|
-
setMembersLoading(false);
|
|
192
|
-
}
|
|
193
260
|
};
|
|
194
261
|
|
|
195
|
-
const handleAddMember =
|
|
196
|
-
if (!
|
|
262
|
+
const handleAddMember = () => {
|
|
263
|
+
if (!selectedTeamId || !selectedUserId) return;
|
|
264
|
+
addMemberMutation.mutate({
|
|
265
|
+
teamId: selectedTeamId,
|
|
266
|
+
userId: selectedUserId,
|
|
267
|
+
});
|
|
268
|
+
};
|
|
197
269
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
toast.success("Member added successfully");
|
|
205
|
-
// Reload team details
|
|
206
|
-
const detail = await authClient.getTeam({
|
|
207
|
-
teamId: selectedTeamDetail.id,
|
|
208
|
-
});
|
|
209
|
-
setSelectedTeamDetail(detail ?? undefined);
|
|
210
|
-
setSelectedUserId("");
|
|
211
|
-
await loadTeams();
|
|
212
|
-
} catch (error) {
|
|
213
|
-
toast.error(
|
|
214
|
-
error instanceof Error ? error.message : "Failed to add member"
|
|
215
|
-
);
|
|
216
|
-
} finally {
|
|
217
|
-
setAddingMember(false);
|
|
218
|
-
}
|
|
270
|
+
const handleRemoveMember = (userId: string) => {
|
|
271
|
+
if (!selectedTeamId) return;
|
|
272
|
+
removeMemberMutation.mutate({
|
|
273
|
+
teamId: selectedTeamId,
|
|
274
|
+
userId,
|
|
275
|
+
});
|
|
219
276
|
};
|
|
220
277
|
|
|
221
|
-
const
|
|
222
|
-
if (!
|
|
278
|
+
const handleToggleManager = (userId: string, isCurrentlyManager: boolean) => {
|
|
279
|
+
if (!selectedTeamId) return;
|
|
223
280
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
teamId:
|
|
281
|
+
if (isCurrentlyManager) {
|
|
282
|
+
removeManagerMutation.mutate({
|
|
283
|
+
teamId: selectedTeamId,
|
|
227
284
|
userId,
|
|
228
285
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
setSelectedTeamDetail(detail ?? undefined);
|
|
235
|
-
await loadTeams();
|
|
236
|
-
} catch (error) {
|
|
237
|
-
toast.error(
|
|
238
|
-
error instanceof Error ? error.message : "Failed to remove member"
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const handleToggleManager = async (
|
|
244
|
-
userId: string,
|
|
245
|
-
isCurrentlyManager: boolean
|
|
246
|
-
) => {
|
|
247
|
-
if (!selectedTeamDetail) return;
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
if (isCurrentlyManager) {
|
|
251
|
-
await authClient.removeTeamManager({
|
|
252
|
-
teamId: selectedTeamDetail.id,
|
|
253
|
-
userId,
|
|
254
|
-
});
|
|
255
|
-
toast.success("Manager role removed");
|
|
256
|
-
} else {
|
|
257
|
-
await authClient.addTeamManager({
|
|
258
|
-
teamId: selectedTeamDetail.id,
|
|
259
|
-
userId,
|
|
260
|
-
});
|
|
261
|
-
toast.success("Member promoted to manager");
|
|
262
|
-
}
|
|
263
|
-
// Reload team details
|
|
264
|
-
const detail = await authClient.getTeam({
|
|
265
|
-
teamId: selectedTeamDetail.id,
|
|
286
|
+
} else {
|
|
287
|
+
addManagerMutation.mutate({
|
|
288
|
+
teamId: selectedTeamId,
|
|
289
|
+
userId,
|
|
266
290
|
});
|
|
267
|
-
setSelectedTeamDetail(detail ?? undefined);
|
|
268
|
-
} catch (error) {
|
|
269
|
-
toast.error(
|
|
270
|
-
error instanceof Error
|
|
271
|
-
? error.message
|
|
272
|
-
: "Failed to update manager status"
|
|
273
|
-
);
|
|
274
291
|
}
|
|
275
292
|
};
|
|
276
293
|
|
|
277
294
|
// Get users not already in the team
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
)
|
|
295
|
+
const teamDetailData = selectedTeamDetail as TeamDetail | undefined;
|
|
296
|
+
const availableUsers = teamDetailData
|
|
297
|
+
? users.filter((u) => !teamDetailData.members.some((m) => m.id === u.id))
|
|
282
298
|
: [];
|
|
283
299
|
|
|
300
|
+
const formSaving =
|
|
301
|
+
createTeamMutation.isPending || updateTeamMutation.isPending;
|
|
302
|
+
const addingMember = addMemberMutation.isPending;
|
|
303
|
+
|
|
284
304
|
return (
|
|
285
305
|
<>
|
|
286
306
|
<Card>
|
|
@@ -299,7 +319,7 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
299
319
|
<div className="flex justify-center py-8">
|
|
300
320
|
<LoadingSpinner />
|
|
301
321
|
</div>
|
|
302
|
-
) : teams.length === 0 ? (
|
|
322
|
+
) : (teams as Team[]).length === 0 ? (
|
|
303
323
|
<p className="text-muted-foreground">No teams found.</p>
|
|
304
324
|
) : (
|
|
305
325
|
<Table>
|
|
@@ -311,7 +331,7 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
311
331
|
</TableRow>
|
|
312
332
|
</TableHeader>
|
|
313
333
|
<TableBody>
|
|
314
|
-
{teams.map((team) => (
|
|
334
|
+
{(teams as Team[]).map((team) => (
|
|
315
335
|
<TableRow key={team.id}>
|
|
316
336
|
<TableCell>
|
|
317
337
|
<div className="flex flex-col">
|
|
@@ -370,7 +390,7 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
370
390
|
)
|
|
371
391
|
) : (
|
|
372
392
|
<p className="text-muted-foreground">
|
|
373
|
-
You don't have
|
|
393
|
+
You don't have access to view teams.
|
|
374
394
|
</p>
|
|
375
395
|
)}
|
|
376
396
|
</CardContent>
|
|
@@ -429,9 +449,7 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
429
449
|
<Dialog open={membersDialogOpen} onOpenChange={setMembersDialogOpen}>
|
|
430
450
|
<DialogContent className="max-w-lg">
|
|
431
451
|
<DialogHeader>
|
|
432
|
-
<DialogTitle>
|
|
433
|
-
{selectedTeamDetail?.name ?? "Team"} Members
|
|
434
|
-
</DialogTitle>
|
|
452
|
+
<DialogTitle>{teamDetailData?.name ?? "Team"} Members</DialogTitle>
|
|
435
453
|
<DialogDescription>
|
|
436
454
|
Manage team membership and assign managers.
|
|
437
455
|
</DialogDescription>
|
|
@@ -441,10 +459,10 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
441
459
|
<div className="flex justify-center py-8">
|
|
442
460
|
<LoadingSpinner />
|
|
443
461
|
</div>
|
|
444
|
-
) :
|
|
462
|
+
) : teamDetailData ? (
|
|
445
463
|
<div className="space-y-4">
|
|
446
464
|
{/* Add Member Form */}
|
|
447
|
-
{(
|
|
465
|
+
{(teamDetailData.managers.some((m) =>
|
|
448
466
|
users.some((u) => u.id === m.id)
|
|
449
467
|
) ||
|
|
450
468
|
canManageTeams) && (
|
|
@@ -483,13 +501,13 @@ export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
|
483
501
|
|
|
484
502
|
{/* Member List */}
|
|
485
503
|
<div className="border rounded-lg divide-y max-h-64 overflow-y-auto">
|
|
486
|
-
{
|
|
504
|
+
{teamDetailData.members.length === 0 ? (
|
|
487
505
|
<p className="text-muted-foreground text-center py-4">
|
|
488
506
|
No members yet
|
|
489
507
|
</p>
|
|
490
508
|
) : (
|
|
491
|
-
|
|
492
|
-
const isManager =
|
|
509
|
+
teamDetailData.members.map((member) => {
|
|
510
|
+
const isManager = teamDetailData.managers.some(
|
|
493
511
|
(m) => m.id === member.id
|
|
494
512
|
);
|
|
495
513
|
return (
|
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
useToast,
|
|
19
19
|
} from "@checkstack/ui";
|
|
20
20
|
import { Plus, Trash2 } from "lucide-react";
|
|
21
|
-
import {
|
|
22
|
-
import { rpcApiRef } from "@checkstack/frontend-api";
|
|
21
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
23
22
|
import { AuthApi } from "@checkstack/auth-common";
|
|
24
23
|
import type { AuthUser, Role, AuthStrategy } from "../api";
|
|
25
24
|
import { CreateUserDialog } from "./CreateUserDialog";
|
|
@@ -47,8 +46,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
47
46
|
canManageRoles,
|
|
48
47
|
onDataChange,
|
|
49
48
|
}) => {
|
|
50
|
-
const
|
|
51
|
-
const authClient = rpcApi.forPlugin(AuthApi);
|
|
49
|
+
const authClient = usePluginClient(AuthApi);
|
|
52
50
|
const toast = useToast();
|
|
53
51
|
|
|
54
52
|
const [userToDelete, setUserToDelete] = useState<string>();
|
|
@@ -58,21 +56,50 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
58
56
|
(s) => s.id === "credential" && s.enabled
|
|
59
57
|
);
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await authClient.deleteUser(userToDelete);
|
|
59
|
+
// Mutations
|
|
60
|
+
const deleteUserMutation = authClient.deleteUser.useMutation({
|
|
61
|
+
onSuccess: () => {
|
|
65
62
|
toast.success("User deleted successfully");
|
|
66
63
|
setUserToDelete(undefined);
|
|
67
|
-
|
|
68
|
-
}
|
|
64
|
+
void onDataChange();
|
|
65
|
+
},
|
|
66
|
+
onError: (error) => {
|
|
69
67
|
toast.error(
|
|
70
68
|
error instanceof Error ? error.message : "Failed to delete user"
|
|
71
69
|
);
|
|
72
|
-
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const updateRolesMutation = authClient.updateUserRoles.useMutation({
|
|
74
|
+
onSuccess: () => {
|
|
75
|
+
void onDataChange();
|
|
76
|
+
},
|
|
77
|
+
onError: (error) => {
|
|
78
|
+
toast.error(
|
|
79
|
+
error instanceof Error ? error.message : "Failed to update roles"
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const createUserMutation = authClient.createCredentialUser.useMutation({
|
|
85
|
+
onSuccess: () => {
|
|
86
|
+
toast.success("User created successfully");
|
|
87
|
+
void onDataChange();
|
|
88
|
+
},
|
|
89
|
+
onError: (error) => {
|
|
90
|
+
toast.error(
|
|
91
|
+
error instanceof Error ? error.message : "Failed to create user"
|
|
92
|
+
);
|
|
93
|
+
throw error;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const handleDeleteUser = () => {
|
|
98
|
+
if (!userToDelete) return;
|
|
99
|
+
deleteUserMutation.mutate(userToDelete);
|
|
73
100
|
};
|
|
74
101
|
|
|
75
|
-
const handleToggleRole =
|
|
102
|
+
const handleToggleRole = (
|
|
76
103
|
userId: string,
|
|
77
104
|
roleId: string,
|
|
78
105
|
currentRoles: string[]
|
|
@@ -86,14 +113,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
86
113
|
? currentRoles.filter((r) => r !== roleId)
|
|
87
114
|
: [...currentRoles, roleId];
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
await authClient.updateUserRoles({ userId, roles: newRoles });
|
|
91
|
-
await onDataChange();
|
|
92
|
-
} catch (error: unknown) {
|
|
93
|
-
const message =
|
|
94
|
-
error instanceof Error ? error.message : "Failed to update roles";
|
|
95
|
-
toast.error(message);
|
|
96
|
-
}
|
|
116
|
+
updateRolesMutation.mutate({ userId, roles: newRoles });
|
|
97
117
|
};
|
|
98
118
|
|
|
99
119
|
const handleCreateUser = async (data: {
|
|
@@ -101,16 +121,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
101
121
|
email: string;
|
|
102
122
|
password: string;
|
|
103
123
|
}) => {
|
|
104
|
-
|
|
105
|
-
await authClient.createCredentialUser(data);
|
|
106
|
-
toast.success("User created successfully");
|
|
107
|
-
await onDataChange();
|
|
108
|
-
} catch (error: unknown) {
|
|
109
|
-
toast.error(
|
|
110
|
-
error instanceof Error ? error.message : "Failed to create user"
|
|
111
|
-
);
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
124
|
+
createUserMutation.mutate(data);
|
|
114
125
|
};
|
|
115
126
|
|
|
116
127
|
return (
|
|
@@ -130,7 +141,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
130
141
|
<AlertDescription>
|
|
131
142
|
You cannot modify roles for your own account. This security
|
|
132
143
|
measure prevents accidental self-lockout from the system and
|
|
133
|
-
|
|
144
|
+
access elevation.
|
|
134
145
|
</AlertDescription>
|
|
135
146
|
</Alert>
|
|
136
147
|
{canReadUsers ? (
|
|
@@ -210,7 +221,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
210
221
|
)
|
|
211
222
|
) : (
|
|
212
223
|
<p className="text-muted-foreground">
|
|
213
|
-
You don't have
|
|
224
|
+
You don't have access to list users.
|
|
214
225
|
</p>
|
|
215
226
|
)}
|
|
216
227
|
</CardContent>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
2
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
3
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
4
|
+
|
|
5
|
+
export const useAccessRules = () => {
|
|
6
|
+
const authBetterClient = useAuthClient();
|
|
7
|
+
const authClient = usePluginClient(AuthApi);
|
|
8
|
+
const { data: session, isPending: sessionPending } =
|
|
9
|
+
authBetterClient.useSession();
|
|
10
|
+
|
|
11
|
+
// Query: Fetch access rules (only when user is authenticated)
|
|
12
|
+
const { data, isLoading } = authClient.accessRules.useQuery(
|
|
13
|
+
{},
|
|
14
|
+
{
|
|
15
|
+
enabled: !sessionPending && !!session?.user,
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// If no session or pending, return empty access rules
|
|
20
|
+
if (sessionPending) {
|
|
21
|
+
return { accessRules: [], loading: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!session?.user) {
|
|
25
|
+
return { accessRules: [], loading: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
accessRules: data?.accessRules ?? [],
|
|
30
|
+
loading: isLoading,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useApi, rpcApiRef } from "@checkstack/frontend-api";
|
|
1
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
2
|
import { AuthApi } from "@checkstack/auth-common";
|
|
4
3
|
import type { EnabledAuthStrategy } from "../api";
|
|
5
4
|
|
|
@@ -10,45 +9,15 @@ export interface UseEnabledStrategiesResult {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export const useEnabledStrategies = (): UseEnabledStrategiesResult => {
|
|
13
|
-
const
|
|
14
|
-
const authClient = rpcApi.forPlugin(AuthApi);
|
|
12
|
+
const authClient = usePluginClient(AuthApi);
|
|
15
13
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const { data, isLoading, error } = authClient.getEnabledStrategies.useQuery(
|
|
15
|
+
{}
|
|
16
|
+
);
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
setLoading(true);
|
|
26
|
-
const result = await authClient.getEnabledStrategies();
|
|
27
|
-
if (mounted) {
|
|
28
|
-
setStrategies(result);
|
|
29
|
-
setError(undefined);
|
|
30
|
-
}
|
|
31
|
-
} catch (error_) {
|
|
32
|
-
if (mounted) {
|
|
33
|
-
setError(
|
|
34
|
-
error_ instanceof Error
|
|
35
|
-
? error_
|
|
36
|
-
: new Error("Failed to fetch strategies")
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
} finally {
|
|
40
|
-
if (mounted) {
|
|
41
|
-
setLoading(false);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
fetchStrategies();
|
|
47
|
-
|
|
48
|
-
return () => {
|
|
49
|
-
mounted = false;
|
|
50
|
-
};
|
|
51
|
-
}, [authClient]);
|
|
52
|
-
|
|
53
|
-
return { strategies, loading, error };
|
|
18
|
+
return {
|
|
19
|
+
strategies: (data ?? []) as EnabledAuthStrategy[],
|
|
20
|
+
loading: isLoading,
|
|
21
|
+
error: error ?? undefined,
|
|
22
|
+
};
|
|
54
23
|
};
|