@checkstack/auth-frontend 0.0.2

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.
@@ -0,0 +1,284 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ DialogFooter,
8
+ Button,
9
+ Input,
10
+ Label,
11
+ Checkbox,
12
+ Accordion,
13
+ AccordionContent,
14
+ AccordionItem,
15
+ AccordionTrigger,
16
+ Badge,
17
+ Alert,
18
+ AlertDescription,
19
+ } from "@checkstack/ui";
20
+ import { Check } from "lucide-react";
21
+ import type { Role, Permission } from "../api";
22
+
23
+ interface RoleDialogProps {
24
+ open: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ role?: Role;
27
+ permissions: Permission[];
28
+ /** Whether current user has this role (prevents permission elevation) */
29
+ isUserRole?: boolean;
30
+ onSave: (params: {
31
+ id?: string;
32
+ name: string;
33
+ description?: string;
34
+ permissions: string[];
35
+ }) => Promise<void>;
36
+ }
37
+
38
+ export const RoleDialog: React.FC<RoleDialogProps> = ({
39
+ open,
40
+ onOpenChange,
41
+ role,
42
+ permissions,
43
+ isUserRole = false,
44
+ onSave,
45
+ }) => {
46
+ const [name, setName] = useState(role?.name || "");
47
+ const [description, setDescription] = useState(role?.description || "");
48
+ const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
49
+ new Set(role?.permissions || [])
50
+ );
51
+ const [saving, setSaving] = useState(false);
52
+
53
+ // Sync state when role prop changes (e.g., opening dialog with different role)
54
+ React.useEffect(() => {
55
+ setName(role?.name || "");
56
+ setDescription(role?.description || "");
57
+ setSelectedPermissions(new Set(role?.permissions || []));
58
+ }, [role]);
59
+
60
+ const isEditing = !!role;
61
+ const isAdminRole = role?.id === "admin";
62
+ // Disable permissions for admin (wildcard) or user's own roles (prevent elevation)
63
+ const permissionsDisabled = isAdminRole || isUserRole;
64
+
65
+ // Group permissions by plugin
66
+ const permissionsByPlugin: Record<string, Permission[]> = {};
67
+ for (const perm of permissions) {
68
+ const [plugin] = perm.id.split(".");
69
+ if (!permissionsByPlugin[plugin]) {
70
+ permissionsByPlugin[plugin] = [];
71
+ }
72
+ permissionsByPlugin[plugin].push(perm);
73
+ }
74
+
75
+ const handleTogglePermission = (permissionId: string) => {
76
+ const newSelected = new Set(selectedPermissions);
77
+ if (newSelected.has(permissionId)) {
78
+ newSelected.delete(permissionId);
79
+ } else {
80
+ newSelected.add(permissionId);
81
+ }
82
+ setSelectedPermissions(newSelected);
83
+ };
84
+
85
+ const handleSave = async () => {
86
+ setSaving(true);
87
+ try {
88
+ await onSave({
89
+ ...(isEditing && { id: role.id }),
90
+ name,
91
+ description: description || undefined,
92
+ permissions: [...selectedPermissions],
93
+ });
94
+ onOpenChange(false);
95
+ } catch (error) {
96
+ console.error("Failed to save role:", error);
97
+ } finally {
98
+ setSaving(false);
99
+ }
100
+ };
101
+
102
+ let buttonText = "Create";
103
+ if (saving) {
104
+ buttonText = "Saving...";
105
+ } else if (isEditing) {
106
+ buttonText = "Update";
107
+ }
108
+
109
+ return (
110
+ <Dialog open={open} onOpenChange={onOpenChange}>
111
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
112
+ <DialogHeader>
113
+ <DialogTitle>{isEditing ? "Edit Role" : "Create Role"}</DialogTitle>
114
+ </DialogHeader>
115
+
116
+ <div className="space-y-4">
117
+ <div>
118
+ <Label htmlFor="role-name">Name</Label>
119
+ <Input
120
+ id="role-name"
121
+ value={name}
122
+ onChange={(e) => setName(e.target.value)}
123
+ placeholder="Developer"
124
+ />
125
+ </div>
126
+
127
+ <div>
128
+ <Label htmlFor="role-description">Description (Optional)</Label>
129
+ <Input
130
+ id="role-description"
131
+ value={description}
132
+ onChange={(e) => setDescription(e.target.value)}
133
+ placeholder="Developers with read/write access"
134
+ />
135
+ </div>
136
+
137
+ <div>
138
+ <Label className="text-base">Permissions</Label>
139
+ <p className="text-sm text-muted-foreground mt-1 mb-3">
140
+ Select permissions to grant to this role. Permissions are
141
+ organized by plugin.
142
+ </p>
143
+ {isAdminRole && (
144
+ <Alert variant="info" className="mb-3">
145
+ <AlertDescription>
146
+ The administrator role has wildcard access to all permissions.
147
+ These cannot be modified.
148
+ </AlertDescription>
149
+ </Alert>
150
+ )}
151
+ {!isAdminRole && isUserRole && (
152
+ <Alert variant="info" className="mb-3">
153
+ <AlertDescription>
154
+ You cannot modify permissions for a role you currently have.
155
+ This prevents accidental self-lockout from the system.
156
+ </AlertDescription>
157
+ </Alert>
158
+ )}
159
+ <div className="border rounded-lg">
160
+ <Accordion
161
+ type="multiple"
162
+ defaultValue={Object.keys(permissionsByPlugin)}
163
+ className="w-full"
164
+ >
165
+ {Object.entries(permissionsByPlugin).map(([plugin, perms]) => (
166
+ <AccordionItem key={plugin} value={plugin}>
167
+ <AccordionTrigger className="px-4 hover:no-underline">
168
+ <div className="flex items-center justify-between flex-1 pr-2">
169
+ <span className="font-semibold capitalize">
170
+ {plugin.replaceAll("-", " ")}
171
+ </span>
172
+ <span className="text-xs text-muted-foreground">
173
+ {
174
+ perms.filter((p) => selectedPermissions.has(p.id))
175
+ .length
176
+ }{" "}
177
+ / {perms.length} selected
178
+ </span>
179
+ </div>
180
+ </AccordionTrigger>
181
+ <AccordionContent className="px-4">
182
+ <div
183
+ className={`space-y-${
184
+ permissionsDisabled ? "2" : "3"
185
+ } pt-2`}
186
+ >
187
+ {perms.map((perm) => {
188
+ const isAssigned =
189
+ isAdminRole || selectedPermissions.has(perm.id);
190
+
191
+ // Use view-style design when permissions are disabled
192
+ if (permissionsDisabled) {
193
+ return (
194
+ <div
195
+ key={perm.id}
196
+ className={`flex items-start space-x-3 p-3 rounded-md transition-colors ${
197
+ isAssigned
198
+ ? "bg-success/10 border border-success/20"
199
+ : "bg-muted/30"
200
+ }`}
201
+ >
202
+ <div className="mt-0.5">
203
+ {isAssigned ? (
204
+ <Check
205
+ className="h-4 w-4 text-success"
206
+ strokeWidth={3}
207
+ />
208
+ ) : (
209
+ <div className="h-4 w-4" />
210
+ )}
211
+ </div>
212
+ <div className="flex-1 space-y-1">
213
+ <div className="flex items-center gap-2">
214
+ <div className="font-medium text-sm">
215
+ {perm.id}
216
+ </div>
217
+ {isAssigned && (
218
+ <Badge
219
+ variant="success"
220
+ className="text-xs"
221
+ >
222
+ Assigned
223
+ </Badge>
224
+ )}
225
+ </div>
226
+ {perm.description && (
227
+ <div className="text-xs text-muted-foreground">
228
+ {perm.description}
229
+ </div>
230
+ )}
231
+ </div>
232
+ </div>
233
+ );
234
+ }
235
+
236
+ // Use editable checkbox design when permissions are editable
237
+ return (
238
+ <div
239
+ key={perm.id}
240
+ className="flex items-start space-x-3 p-2 rounded-md hover:bg-muted/50 transition-colors"
241
+ >
242
+ <Checkbox
243
+ id={`perm-${perm.id}`}
244
+ checked={selectedPermissions.has(perm.id)}
245
+ onCheckedChange={() =>
246
+ handleTogglePermission(perm.id)
247
+ }
248
+ className="mt-0.5"
249
+ />
250
+ <label
251
+ htmlFor={`perm-${perm.id}`}
252
+ className="text-sm cursor-pointer flex-1 space-y-1"
253
+ >
254
+ <div className="font-medium">{perm.id}</div>
255
+ {perm.description && (
256
+ <div className="text-xs text-muted-foreground">
257
+ {perm.description}
258
+ </div>
259
+ )}
260
+ </label>
261
+ </div>
262
+ );
263
+ })}
264
+ </div>
265
+ </AccordionContent>
266
+ </AccordionItem>
267
+ ))}
268
+ </Accordion>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <DialogFooter>
274
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
275
+ Cancel
276
+ </Button>
277
+ <Button onClick={handleSave} disabled={saving || !name}>
278
+ {buttonText}
279
+ </Button>
280
+ </DialogFooter>
281
+ </DialogContent>
282
+ </Dialog>
283
+ );
284
+ };
@@ -0,0 +1,219 @@
1
+ import React, { useState } 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
+ } from "@checkstack/ui";
18
+ import { Plus, Edit, Trash2 } from "lucide-react";
19
+ import { useApi } from "@checkstack/frontend-api";
20
+ import { rpcApiRef } from "@checkstack/frontend-api";
21
+ import { AuthApi } from "@checkstack/auth-common";
22
+ import type { Role, Permission } from "../api";
23
+ import { RoleDialog } from "./RoleDialog";
24
+
25
+ export interface RolesTabProps {
26
+ roles: Role[];
27
+ permissions: Permission[];
28
+ userRoleIds: string[];
29
+ canReadRoles: boolean;
30
+ canCreateRoles: boolean;
31
+ canUpdateRoles: boolean;
32
+ canDeleteRoles: boolean;
33
+ onDataChange: () => Promise<void>;
34
+ }
35
+
36
+ export const RolesTab: React.FC<RolesTabProps> = ({
37
+ roles,
38
+ permissions,
39
+ userRoleIds,
40
+ canReadRoles,
41
+ canCreateRoles,
42
+ canUpdateRoles,
43
+ canDeleteRoles,
44
+ onDataChange,
45
+ }) => {
46
+ const rpcApi = useApi(rpcApiRef);
47
+ const authClient = rpcApi.forPlugin(AuthApi);
48
+ const toast = useToast();
49
+
50
+ const [roleToDelete, setRoleToDelete] = useState<string>();
51
+ const [roleDialogOpen, setRoleDialogOpen] = useState(false);
52
+ const [editingRole, setEditingRole] = useState<Role | undefined>();
53
+
54
+ const handleCreateRole = () => {
55
+ setEditingRole(undefined);
56
+ setRoleDialogOpen(true);
57
+ };
58
+
59
+ const handleEditRole = (role: Role) => {
60
+ setEditingRole(role);
61
+ setRoleDialogOpen(true);
62
+ };
63
+
64
+ const handleSaveRole = async (params: {
65
+ id?: string;
66
+ name: string;
67
+ description?: string;
68
+ permissions: string[];
69
+ }) => {
70
+ try {
71
+ if (params.id) {
72
+ await authClient.updateRole({
73
+ id: params.id,
74
+ name: params.name,
75
+ description: params.description,
76
+ permissions: params.permissions,
77
+ });
78
+ toast.success("Role updated successfully");
79
+ } else {
80
+ await authClient.createRole({
81
+ name: params.name,
82
+ description: params.description,
83
+ permissions: params.permissions,
84
+ });
85
+ toast.success("Role created successfully");
86
+ }
87
+ await onDataChange();
88
+ } catch (error: unknown) {
89
+ toast.error(
90
+ error instanceof Error ? error.message : "Failed to save role"
91
+ );
92
+ throw error;
93
+ }
94
+ };
95
+
96
+ const handleDeleteRole = async () => {
97
+ if (!roleToDelete) return;
98
+ try {
99
+ await authClient.deleteRole(roleToDelete);
100
+ toast.success("Role deleted successfully");
101
+ setRoleToDelete(undefined);
102
+ await onDataChange();
103
+ } catch (error: unknown) {
104
+ toast.error(
105
+ error instanceof Error ? error.message : "Failed to delete role"
106
+ );
107
+ }
108
+ };
109
+
110
+ return (
111
+ <>
112
+ <Card>
113
+ <CardHeader className="flex flex-row items-center justify-between">
114
+ <CardTitle>Role Management</CardTitle>
115
+ {canCreateRoles && (
116
+ <Button onClick={handleCreateRole} size="sm">
117
+ <Plus className="h-4 w-4 mr-2" />
118
+ Create Role
119
+ </Button>
120
+ )}
121
+ </CardHeader>
122
+ <CardContent>
123
+ {canReadRoles ? (
124
+ roles.length === 0 ? (
125
+ <p className="text-muted-foreground">No roles found.</p>
126
+ ) : (
127
+ <Table>
128
+ <TableHeader>
129
+ <TableRow>
130
+ <TableHead>Role</TableHead>
131
+ <TableHead>Permissions</TableHead>
132
+ <TableHead className="text-right">Actions</TableHead>
133
+ </TableRow>
134
+ </TableHeader>
135
+ <TableBody>
136
+ {roles.map((role) => {
137
+ const isUserRole = userRoleIds.includes(role.id);
138
+ const isSystem = role.isSystem;
139
+
140
+ return (
141
+ <TableRow key={role.id}>
142
+ <TableCell>
143
+ <div className="flex flex-col">
144
+ <span className="font-medium">{role.name}</span>
145
+ {role.description && (
146
+ <span className="text-sm text-muted-foreground">
147
+ {role.description}
148
+ </span>
149
+ )}
150
+ <div className="flex gap-2 mt-1">
151
+ {isSystem && (
152
+ <Badge variant="outline">System</Badge>
153
+ )}
154
+ {isUserRole && (
155
+ <Badge variant="secondary">Your Role</Badge>
156
+ )}
157
+ </div>
158
+ </div>
159
+ </TableCell>
160
+ <TableCell>
161
+ <span className="text-sm text-muted-foreground">
162
+ {role.permissions?.length || 0} permissions
163
+ </span>
164
+ </TableCell>
165
+ <TableCell className="text-right">
166
+ <div className="flex justify-end gap-2">
167
+ <Button
168
+ variant="ghost"
169
+ size="sm"
170
+ onClick={() => handleEditRole(role)}
171
+ disabled={!canUpdateRoles}
172
+ >
173
+ <Edit className="h-4 w-4" />
174
+ </Button>
175
+ <Button
176
+ variant="ghost"
177
+ size="sm"
178
+ onClick={() => setRoleToDelete(role.id)}
179
+ disabled={
180
+ isSystem || isUserRole || !canDeleteRoles
181
+ }
182
+ >
183
+ <Trash2 className="h-4 w-4" />
184
+ </Button>
185
+ </div>
186
+ </TableCell>
187
+ </TableRow>
188
+ );
189
+ })}
190
+ </TableBody>
191
+ </Table>
192
+ )
193
+ ) : (
194
+ <p className="text-muted-foreground">
195
+ You don't have permission to view roles.
196
+ </p>
197
+ )}
198
+ </CardContent>
199
+ </Card>
200
+
201
+ <RoleDialog
202
+ open={roleDialogOpen}
203
+ onOpenChange={setRoleDialogOpen}
204
+ role={editingRole}
205
+ permissions={permissions}
206
+ isUserRole={editingRole ? userRoleIds.includes(editingRole.id) : false}
207
+ onSave={handleSaveRole}
208
+ />
209
+
210
+ <ConfirmationModal
211
+ isOpen={!!roleToDelete}
212
+ onClose={() => setRoleToDelete(undefined)}
213
+ onConfirm={handleDeleteRole}
214
+ title="Delete Role"
215
+ message="Are you sure you want to delete this role? This action cannot be undone."
216
+ />
217
+ </>
218
+ );
219
+ };
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import {
3
+ Button,
4
+ DynamicIcon,
5
+ type LucideIconName,
6
+ } from "@checkstack/ui";
7
+
8
+ interface SocialProviderButtonProps {
9
+ displayName: string;
10
+ icon?: LucideIconName;
11
+ onClick: () => void;
12
+ }
13
+
14
+ export const SocialProviderButton: React.FC<SocialProviderButtonProps> = ({
15
+ displayName,
16
+ icon,
17
+ onClick,
18
+ }) => {
19
+ return (
20
+ <Button
21
+ type="button"
22
+ variant="outline"
23
+ className="w-full"
24
+ onClick={onClick}
25
+ >
26
+ <DynamicIcon name={icon} className="h-4 w-4" />
27
+ <span className="ml-2">Continue with {displayName}</span>
28
+ </Button>
29
+ );
30
+ };