@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.
- package/CHANGELOG.md +207 -0
- package/e2e/login.e2e.ts +63 -0
- package/package.json +34 -0
- package/playwright-report/data/774b616fd991c36e57f6aa95d67906b877dff5d1.md +20 -0
- package/playwright-report/data/d37ef869a8ef03c489f7ca3b80d67da69614c383.png +3 -0
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +5 -0
- package/src/api.ts +78 -0
- package/src/components/ApplicationsTab.tsx +452 -0
- package/src/components/AuthErrorPage.tsx +94 -0
- package/src/components/AuthSettingsPage.tsx +249 -0
- package/src/components/AuthStrategyCard.tsx +77 -0
- package/src/components/ChangePasswordPage.tsx +259 -0
- package/src/components/CreateUserDialog.tsx +156 -0
- package/src/components/ForgotPasswordPage.tsx +131 -0
- package/src/components/LoginPage.tsx +330 -0
- package/src/components/RegisterPage.tsx +350 -0
- package/src/components/ResetPasswordPage.tsx +262 -0
- package/src/components/RoleDialog.tsx +284 -0
- package/src/components/RolesTab.tsx +219 -0
- package/src/components/SocialProviderButton.tsx +30 -0
- package/src/components/StrategiesTab.tsx +276 -0
- package/src/components/UsersTab.tsx +234 -0
- package/src/hooks/useEnabledStrategies.ts +54 -0
- package/src/hooks/usePermissions.ts +43 -0
- package/src/index.test.tsx +95 -0
- package/src/index.tsx +271 -0
- package/src/lib/auth-client.ts +55 -0
- package/test-results/login-Login-Page-should-show-login-form-elements-chromium/test-failed-1.png +3 -0
- package/tsconfig.json +6 -0
|
@@ -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
|
+
};
|