@checkstack/auth-frontend 0.0.3 → 0.1.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 +91 -0
- package/package.json +1 -1
- package/src/components/ApplicationsTab.tsx +9 -2
- package/src/components/AuthSettingsPage.tsx +39 -14
- package/src/components/RoleDialog.tsx +6 -0
- package/src/components/TeamAccessEditor.tsx +560 -0
- package/src/components/TeamsTab.tsx +569 -0
- package/src/index.tsx +6 -5
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardHeader,
|
|
5
|
+
CardTitle,
|
|
6
|
+
CardContent,
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
Button,
|
|
14
|
+
Badge,
|
|
15
|
+
ConfirmationModal,
|
|
16
|
+
useToast,
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogContent,
|
|
19
|
+
DialogDescription,
|
|
20
|
+
DialogHeader,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
DialogFooter,
|
|
23
|
+
Input,
|
|
24
|
+
Label,
|
|
25
|
+
Textarea,
|
|
26
|
+
LoadingSpinner,
|
|
27
|
+
Select,
|
|
28
|
+
SelectContent,
|
|
29
|
+
SelectItem,
|
|
30
|
+
SelectTrigger,
|
|
31
|
+
SelectValue,
|
|
32
|
+
} from "@checkstack/ui";
|
|
33
|
+
import {
|
|
34
|
+
Plus,
|
|
35
|
+
Edit,
|
|
36
|
+
Trash2,
|
|
37
|
+
Users2,
|
|
38
|
+
Crown,
|
|
39
|
+
UserPlus,
|
|
40
|
+
UserMinus,
|
|
41
|
+
} from "lucide-react";
|
|
42
|
+
import { useApi } from "@checkstack/frontend-api";
|
|
43
|
+
import { rpcApiRef } from "@checkstack/frontend-api";
|
|
44
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
45
|
+
import type { AuthUser } from "../api";
|
|
46
|
+
|
|
47
|
+
interface Team {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string | null;
|
|
51
|
+
memberCount: number;
|
|
52
|
+
isManager: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface TeamDetail {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string | null;
|
|
59
|
+
members: Array<{ id: string; name: string; email: string }>;
|
|
60
|
+
managers: Array<{ id: string; name: string; email: string }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TeamsTabProps {
|
|
64
|
+
users: AuthUser[];
|
|
65
|
+
canReadTeams: boolean;
|
|
66
|
+
canManageTeams: boolean;
|
|
67
|
+
onDataChange: () => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const TeamsTab: React.FC<TeamsTabProps> = ({
|
|
71
|
+
users,
|
|
72
|
+
canReadTeams,
|
|
73
|
+
canManageTeams,
|
|
74
|
+
onDataChange,
|
|
75
|
+
}) => {
|
|
76
|
+
const rpcApi = useApi(rpcApiRef);
|
|
77
|
+
const authClient = rpcApi.forPlugin(AuthApi);
|
|
78
|
+
const toast = useToast();
|
|
79
|
+
|
|
80
|
+
const [teams, setTeams] = useState<Team[]>([]);
|
|
81
|
+
const [loading, setLoading] = useState(true);
|
|
82
|
+
const [teamToDelete, setTeamToDelete] = useState<string>();
|
|
83
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
84
|
+
const [editingTeam, setEditingTeam] = useState<Team | undefined>();
|
|
85
|
+
const [membersDialogOpen, setMembersDialogOpen] = useState(false);
|
|
86
|
+
const [selectedTeamDetail, setSelectedTeamDetail] = useState<TeamDetail>();
|
|
87
|
+
const [membersLoading, setMembersLoading] = useState(false);
|
|
88
|
+
|
|
89
|
+
// Team form state
|
|
90
|
+
const [formName, setFormName] = useState("");
|
|
91
|
+
const [formDescription, setFormDescription] = useState("");
|
|
92
|
+
const [formSaving, setFormSaving] = useState(false);
|
|
93
|
+
|
|
94
|
+
// Member management state
|
|
95
|
+
const [selectedUserId, setSelectedUserId] = useState("");
|
|
96
|
+
const [addingMember, setAddingMember] = useState(false);
|
|
97
|
+
|
|
98
|
+
const loadTeams = useCallback(async () => {
|
|
99
|
+
setLoading(true);
|
|
100
|
+
try {
|
|
101
|
+
const teamsData = await authClient.getTeams();
|
|
102
|
+
setTeams(teamsData);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
toast.error(
|
|
105
|
+
error instanceof Error ? error.message : "Failed to load teams"
|
|
106
|
+
);
|
|
107
|
+
} finally {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
}
|
|
110
|
+
}, [authClient, toast]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
loadTeams();
|
|
114
|
+
}, [loadTeams]);
|
|
115
|
+
|
|
116
|
+
const handleCreateTeam = () => {
|
|
117
|
+
setEditingTeam(undefined);
|
|
118
|
+
setFormName("");
|
|
119
|
+
setFormDescription("");
|
|
120
|
+
setEditDialogOpen(true);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleEditTeam = (team: Team) => {
|
|
124
|
+
setEditingTeam(team);
|
|
125
|
+
setFormName(team.name);
|
|
126
|
+
setFormDescription(team.description ?? "");
|
|
127
|
+
setEditDialogOpen(true);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleSaveTeam = async () => {
|
|
131
|
+
if (!formName.trim()) {
|
|
132
|
+
toast.error("Team name is required");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setFormSaving(true);
|
|
137
|
+
try {
|
|
138
|
+
if (editingTeam) {
|
|
139
|
+
await authClient.updateTeam({
|
|
140
|
+
id: editingTeam.id,
|
|
141
|
+
name: formName,
|
|
142
|
+
description: formDescription || undefined,
|
|
143
|
+
});
|
|
144
|
+
toast.success("Team updated successfully");
|
|
145
|
+
} else {
|
|
146
|
+
await authClient.createTeam({
|
|
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);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleDeleteTeam = async () => {
|
|
165
|
+
if (!teamToDelete) return;
|
|
166
|
+
try {
|
|
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
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const openMembersDialog = async (teamId: string) => {
|
|
180
|
+
setMembersLoading(true);
|
|
181
|
+
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
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleAddMember = async () => {
|
|
196
|
+
if (!selectedTeamDetail || !selectedUserId) return;
|
|
197
|
+
|
|
198
|
+
setAddingMember(true);
|
|
199
|
+
try {
|
|
200
|
+
await authClient.addUserToTeam({
|
|
201
|
+
teamId: selectedTeamDetail.id,
|
|
202
|
+
userId: selectedUserId,
|
|
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
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const handleRemoveMember = async (userId: string) => {
|
|
222
|
+
if (!selectedTeamDetail) return;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await authClient.removeUserFromTeam({
|
|
226
|
+
teamId: selectedTeamDetail.id,
|
|
227
|
+
userId,
|
|
228
|
+
});
|
|
229
|
+
toast.success("Member removed");
|
|
230
|
+
// Reload team details
|
|
231
|
+
const detail = await authClient.getTeam({
|
|
232
|
+
teamId: selectedTeamDetail.id,
|
|
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,
|
|
266
|
+
});
|
|
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
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Get users not already in the team
|
|
278
|
+
const availableUsers = selectedTeamDetail
|
|
279
|
+
? users.filter(
|
|
280
|
+
(u) => !selectedTeamDetail.members.some((m) => m.id === u.id)
|
|
281
|
+
)
|
|
282
|
+
: [];
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<Card>
|
|
287
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
288
|
+
<CardTitle>Team Management</CardTitle>
|
|
289
|
+
{canManageTeams && (
|
|
290
|
+
<Button onClick={handleCreateTeam} size="sm">
|
|
291
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
292
|
+
Create Team
|
|
293
|
+
</Button>
|
|
294
|
+
)}
|
|
295
|
+
</CardHeader>
|
|
296
|
+
<CardContent>
|
|
297
|
+
{canReadTeams ? (
|
|
298
|
+
loading ? (
|
|
299
|
+
<div className="flex justify-center py-8">
|
|
300
|
+
<LoadingSpinner />
|
|
301
|
+
</div>
|
|
302
|
+
) : teams.length === 0 ? (
|
|
303
|
+
<p className="text-muted-foreground">No teams found.</p>
|
|
304
|
+
) : (
|
|
305
|
+
<Table>
|
|
306
|
+
<TableHeader>
|
|
307
|
+
<TableRow>
|
|
308
|
+
<TableHead>Team</TableHead>
|
|
309
|
+
<TableHead>Members</TableHead>
|
|
310
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
311
|
+
</TableRow>
|
|
312
|
+
</TableHeader>
|
|
313
|
+
<TableBody>
|
|
314
|
+
{teams.map((team) => (
|
|
315
|
+
<TableRow key={team.id}>
|
|
316
|
+
<TableCell>
|
|
317
|
+
<div className="flex flex-col">
|
|
318
|
+
<span className="font-medium">{team.name}</span>
|
|
319
|
+
{team.description && (
|
|
320
|
+
<span className="text-sm text-muted-foreground">
|
|
321
|
+
{team.description}
|
|
322
|
+
</span>
|
|
323
|
+
)}
|
|
324
|
+
{team.isManager && (
|
|
325
|
+
<Badge variant="secondary" className="mt-1 w-fit">
|
|
326
|
+
<Crown className="h-3 w-3 mr-1" />
|
|
327
|
+
Manager
|
|
328
|
+
</Badge>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</TableCell>
|
|
332
|
+
<TableCell>
|
|
333
|
+
<span className="text-muted-foreground">
|
|
334
|
+
{team.memberCount} member
|
|
335
|
+
{team.memberCount === 1 ? "" : "s"}
|
|
336
|
+
</span>
|
|
337
|
+
</TableCell>
|
|
338
|
+
<TableCell className="text-right">
|
|
339
|
+
<div className="flex justify-end gap-2">
|
|
340
|
+
<Button
|
|
341
|
+
variant="ghost"
|
|
342
|
+
size="sm"
|
|
343
|
+
onClick={() => openMembersDialog(team.id)}
|
|
344
|
+
title="Manage members"
|
|
345
|
+
>
|
|
346
|
+
<Users2 className="h-4 w-4" />
|
|
347
|
+
</Button>
|
|
348
|
+
<Button
|
|
349
|
+
variant="ghost"
|
|
350
|
+
size="sm"
|
|
351
|
+
onClick={() => handleEditTeam(team)}
|
|
352
|
+
disabled={!team.isManager && !canManageTeams}
|
|
353
|
+
>
|
|
354
|
+
<Edit className="h-4 w-4" />
|
|
355
|
+
</Button>
|
|
356
|
+
<Button
|
|
357
|
+
variant="ghost"
|
|
358
|
+
size="sm"
|
|
359
|
+
onClick={() => setTeamToDelete(team.id)}
|
|
360
|
+
disabled={!canManageTeams}
|
|
361
|
+
>
|
|
362
|
+
<Trash2 className="h-4 w-4" />
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</TableCell>
|
|
366
|
+
</TableRow>
|
|
367
|
+
))}
|
|
368
|
+
</TableBody>
|
|
369
|
+
</Table>
|
|
370
|
+
)
|
|
371
|
+
) : (
|
|
372
|
+
<p className="text-muted-foreground">
|
|
373
|
+
You don't have permission to view teams.
|
|
374
|
+
</p>
|
|
375
|
+
)}
|
|
376
|
+
</CardContent>
|
|
377
|
+
</Card>
|
|
378
|
+
|
|
379
|
+
{/* Create/Edit Team Dialog */}
|
|
380
|
+
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
|
381
|
+
<DialogContent>
|
|
382
|
+
<DialogHeader>
|
|
383
|
+
<DialogTitle>
|
|
384
|
+
{editingTeam ? "Edit Team" : "Create Team"}
|
|
385
|
+
</DialogTitle>
|
|
386
|
+
<DialogDescription>
|
|
387
|
+
{editingTeam
|
|
388
|
+
? "Update the team details below."
|
|
389
|
+
: "Create a new team to organize resource access."}
|
|
390
|
+
</DialogDescription>
|
|
391
|
+
</DialogHeader>
|
|
392
|
+
<div className="space-y-4">
|
|
393
|
+
<div>
|
|
394
|
+
<Label htmlFor="name">Name</Label>
|
|
395
|
+
<Input
|
|
396
|
+
id="name"
|
|
397
|
+
value={formName}
|
|
398
|
+
onChange={(e) => setFormName(e.target.value)}
|
|
399
|
+
placeholder="Team name"
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<Label htmlFor="description">Description</Label>
|
|
404
|
+
<Textarea
|
|
405
|
+
id="description"
|
|
406
|
+
value={formDescription}
|
|
407
|
+
onChange={(e) => setFormDescription(e.target.value)}
|
|
408
|
+
placeholder="Optional description"
|
|
409
|
+
rows={3}
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<DialogFooter>
|
|
414
|
+
<Button
|
|
415
|
+
variant="outline"
|
|
416
|
+
onClick={() => setEditDialogOpen(false)}
|
|
417
|
+
disabled={formSaving}
|
|
418
|
+
>
|
|
419
|
+
Cancel
|
|
420
|
+
</Button>
|
|
421
|
+
<Button onClick={handleSaveTeam} disabled={formSaving}>
|
|
422
|
+
{formSaving ? "Saving..." : editingTeam ? "Update" : "Create"}
|
|
423
|
+
</Button>
|
|
424
|
+
</DialogFooter>
|
|
425
|
+
</DialogContent>
|
|
426
|
+
</Dialog>
|
|
427
|
+
|
|
428
|
+
{/* Members Management Dialog */}
|
|
429
|
+
<Dialog open={membersDialogOpen} onOpenChange={setMembersDialogOpen}>
|
|
430
|
+
<DialogContent className="max-w-lg">
|
|
431
|
+
<DialogHeader>
|
|
432
|
+
<DialogTitle>
|
|
433
|
+
{selectedTeamDetail?.name ?? "Team"} Members
|
|
434
|
+
</DialogTitle>
|
|
435
|
+
<DialogDescription>
|
|
436
|
+
Manage team membership and assign managers.
|
|
437
|
+
</DialogDescription>
|
|
438
|
+
</DialogHeader>
|
|
439
|
+
|
|
440
|
+
{membersLoading ? (
|
|
441
|
+
<div className="flex justify-center py-8">
|
|
442
|
+
<LoadingSpinner />
|
|
443
|
+
</div>
|
|
444
|
+
) : selectedTeamDetail ? (
|
|
445
|
+
<div className="space-y-4">
|
|
446
|
+
{/* Add Member Form */}
|
|
447
|
+
{(selectedTeamDetail.managers.some((m) =>
|
|
448
|
+
users.some((u) => u.id === m.id)
|
|
449
|
+
) ||
|
|
450
|
+
canManageTeams) && (
|
|
451
|
+
<div className="flex gap-2">
|
|
452
|
+
<Select
|
|
453
|
+
value={selectedUserId}
|
|
454
|
+
onValueChange={setSelectedUserId}
|
|
455
|
+
>
|
|
456
|
+
<SelectTrigger className="flex-1">
|
|
457
|
+
<SelectValue placeholder="Select user to add" />
|
|
458
|
+
</SelectTrigger>
|
|
459
|
+
<SelectContent>
|
|
460
|
+
{availableUsers.length === 0 ? (
|
|
461
|
+
<SelectItem value="_none" disabled>
|
|
462
|
+
No available users
|
|
463
|
+
</SelectItem>
|
|
464
|
+
) : (
|
|
465
|
+
availableUsers.map((user) => (
|
|
466
|
+
<SelectItem key={user.id} value={user.id}>
|
|
467
|
+
{user.name} ({user.email})
|
|
468
|
+
</SelectItem>
|
|
469
|
+
))
|
|
470
|
+
)}
|
|
471
|
+
</SelectContent>
|
|
472
|
+
</Select>
|
|
473
|
+
<Button
|
|
474
|
+
onClick={handleAddMember}
|
|
475
|
+
disabled={!selectedUserId || addingMember}
|
|
476
|
+
size="sm"
|
|
477
|
+
>
|
|
478
|
+
<UserPlus className="h-4 w-4 mr-1" />
|
|
479
|
+
Add
|
|
480
|
+
</Button>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{/* Member List */}
|
|
485
|
+
<div className="border rounded-lg divide-y max-h-64 overflow-y-auto">
|
|
486
|
+
{selectedTeamDetail.members.length === 0 ? (
|
|
487
|
+
<p className="text-muted-foreground text-center py-4">
|
|
488
|
+
No members yet
|
|
489
|
+
</p>
|
|
490
|
+
) : (
|
|
491
|
+
selectedTeamDetail.members.map((member) => {
|
|
492
|
+
const isManager = selectedTeamDetail.managers.some(
|
|
493
|
+
(m) => m.id === member.id
|
|
494
|
+
);
|
|
495
|
+
return (
|
|
496
|
+
<div
|
|
497
|
+
key={member.id}
|
|
498
|
+
className="flex items-center justify-between p-3"
|
|
499
|
+
>
|
|
500
|
+
<div>
|
|
501
|
+
<div className="font-medium">{member.name}</div>
|
|
502
|
+
<div className="text-sm text-muted-foreground">
|
|
503
|
+
{member.email}
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
<div className="flex items-center gap-2">
|
|
507
|
+
{isManager && (
|
|
508
|
+
<Badge variant="secondary">
|
|
509
|
+
<Crown className="h-3 w-3 mr-1" />
|
|
510
|
+
Manager
|
|
511
|
+
</Badge>
|
|
512
|
+
)}
|
|
513
|
+
{canManageTeams && (
|
|
514
|
+
<>
|
|
515
|
+
<Button
|
|
516
|
+
variant="ghost"
|
|
517
|
+
size="sm"
|
|
518
|
+
onClick={() =>
|
|
519
|
+
handleToggleManager(member.id, isManager)
|
|
520
|
+
}
|
|
521
|
+
title={
|
|
522
|
+
isManager
|
|
523
|
+
? "Remove manager role"
|
|
524
|
+
: "Promote to manager"
|
|
525
|
+
}
|
|
526
|
+
>
|
|
527
|
+
<Crown
|
|
528
|
+
className={`h-4 w-4 ${
|
|
529
|
+
isManager ? "text-warning" : ""
|
|
530
|
+
}`}
|
|
531
|
+
/>
|
|
532
|
+
</Button>
|
|
533
|
+
<Button
|
|
534
|
+
variant="ghost"
|
|
535
|
+
size="sm"
|
|
536
|
+
onClick={() => handleRemoveMember(member.id)}
|
|
537
|
+
title="Remove from team"
|
|
538
|
+
>
|
|
539
|
+
<UserMinus className="h-4 w-4" />
|
|
540
|
+
</Button>
|
|
541
|
+
</>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
})
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
) : undefined}
|
|
551
|
+
|
|
552
|
+
<DialogFooter>
|
|
553
|
+
<Button onClick={() => setMembersDialogOpen(false)}>Close</Button>
|
|
554
|
+
</DialogFooter>
|
|
555
|
+
</DialogContent>
|
|
556
|
+
</Dialog>
|
|
557
|
+
|
|
558
|
+
{/* Delete Confirmation Dialog */}
|
|
559
|
+
<ConfirmationModal
|
|
560
|
+
isOpen={!!teamToDelete}
|
|
561
|
+
onClose={() => setTeamToDelete(undefined)}
|
|
562
|
+
onConfirm={handleDeleteTeam}
|
|
563
|
+
title="Delete Team"
|
|
564
|
+
message="Are you sure you want to delete this team? All resource access grants associated with this team will be removed. This action cannot be undone."
|
|
565
|
+
variant="danger"
|
|
566
|
+
/>
|
|
567
|
+
</>
|
|
568
|
+
);
|
|
569
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -24,10 +24,7 @@ import { getAuthClientLazy } from "./lib/auth-client";
|
|
|
24
24
|
|
|
25
25
|
import { usePermissions } from "./hooks/usePermissions";
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
PermissionAction,
|
|
29
|
-
qualifyPermissionId,
|
|
30
|
-
} from "@checkstack/common";
|
|
27
|
+
import { PermissionAction, qualifyPermissionId } from "@checkstack/common";
|
|
31
28
|
import { useNavigate } from "react-router-dom";
|
|
32
29
|
import { Settings2, Key } from "lucide-react";
|
|
33
30
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
@@ -125,7 +122,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
125
122
|
|
|
126
123
|
async signInWithSocial(provider: string) {
|
|
127
124
|
// Use current origin as callback URL (works in dev and production)
|
|
128
|
-
const frontendUrl = globalThis.location?.origin
|
|
125
|
+
const frontendUrl = globalThis.location?.origin;
|
|
129
126
|
await getAuthClientLazy().signIn.social({
|
|
130
127
|
provider,
|
|
131
128
|
callbackURL: frontendUrl,
|
|
@@ -171,6 +168,10 @@ class BetterAuthApi implements AuthApi {
|
|
|
171
168
|
}
|
|
172
169
|
}
|
|
173
170
|
|
|
171
|
+
// Re-export TeamAccessEditor for use in other plugins
|
|
172
|
+
export { TeamAccessEditor } from "./components/TeamAccessEditor";
|
|
173
|
+
export type { TeamAccessEditorProps } from "./components/TeamAccessEditor";
|
|
174
|
+
|
|
174
175
|
export const authPlugin = createFrontendPlugin({
|
|
175
176
|
metadata: pluginMetadata,
|
|
176
177
|
apis: [
|