vperms-testing 1.0.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/bun.lock +94 -0
- package/frontend/bun.lock +252 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +22 -0
- package/frontend/src/App.jsx +79 -0
- package/frontend/src/components/Layout.jsx +58 -0
- package/frontend/src/context/AuthContext.jsx +7 -0
- package/frontend/src/hooks/useApi.jsx +35 -0
- package/frontend/src/index.css +462 -0
- package/frontend/src/main.jsx +10 -0
- package/frontend/src/pages/Dashboard.jsx +148 -0
- package/frontend/src/pages/Login.jsx +109 -0
- package/frontend/src/pages/Permissions.jsx +150 -0
- package/frontend/src/pages/Roles.jsx +263 -0
- package/frontend/src/pages/Users.jsx +171 -0
- package/frontend/vite.config.js +15 -0
- package/package.json +25 -0
- package/prisma/schema.prisma +104 -0
- package/query +0 -0
- package/server/index.js +57 -0
- package/server/middleware/auth.js +65 -0
- package/server/routes/auth.js +157 -0
- package/server/routes/permissions.js +64 -0
- package/server/routes/roles.js +208 -0
- package/server/routes/users.js +191 -0
- package/server/seed.js +167 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useAuth } from '../context/AuthContext';
|
|
3
|
+
|
|
4
|
+
export default function Login() {
|
|
5
|
+
const { login } = useAuth();
|
|
6
|
+
const [isRegister, setIsRegister] = useState(false);
|
|
7
|
+
const [email, setEmail] = useState('');
|
|
8
|
+
const [password, setPassword] = useState('');
|
|
9
|
+
const [name, setName] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setError('');
|
|
16
|
+
setLoading(true);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login';
|
|
20
|
+
const body = isRegister ? { email, password, name } : { email, password };
|
|
21
|
+
|
|
22
|
+
const response = await fetch(endpoint, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(data.error || 'Authentication failed');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
login(data.user, data.token);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err.message);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="auth-container">
|
|
44
|
+
<div className="auth-card">
|
|
45
|
+
<h1 className="auth-title">🔐 v-perms</h1>
|
|
46
|
+
<p className="auth-subtitle">
|
|
47
|
+
{isRegister ? 'Create your account' : 'Sign in to your account'}
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
{error && <div className="message message-error">{error}</div>}
|
|
51
|
+
|
|
52
|
+
<form onSubmit={handleSubmit}>
|
|
53
|
+
{isRegister && (
|
|
54
|
+
<div className="form-group">
|
|
55
|
+
<label className="form-label">Name</label>
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
className="form-input"
|
|
59
|
+
placeholder="Your name"
|
|
60
|
+
value={name}
|
|
61
|
+
onChange={(e) => setName(e.target.value)}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<div className="form-group">
|
|
67
|
+
<label className="form-label">Email</label>
|
|
68
|
+
<input
|
|
69
|
+
type="email"
|
|
70
|
+
className="form-input"
|
|
71
|
+
placeholder="you@example.com"
|
|
72
|
+
value={email}
|
|
73
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
74
|
+
required
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="form-group">
|
|
79
|
+
<label className="form-label">Password</label>
|
|
80
|
+
<input
|
|
81
|
+
type="password"
|
|
82
|
+
className="form-input"
|
|
83
|
+
placeholder="••••••••"
|
|
84
|
+
value={password}
|
|
85
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
86
|
+
required
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<button type="submit" className="btn btn-primary" style={{ width: '100%' }} disabled={loading}>
|
|
91
|
+
{loading ? 'Please wait...' : (isRegister ? 'Create Account' : 'Sign In')}
|
|
92
|
+
</button>
|
|
93
|
+
</form>
|
|
94
|
+
|
|
95
|
+
<p className="auth-switch">
|
|
96
|
+
{isRegister ? 'Already have an account? ' : "Don't have an account? "}
|
|
97
|
+
<a href="#" onClick={(e) => { e.preventDefault(); setIsRegister(!isRegister); setError(''); }}>
|
|
98
|
+
{isRegister ? 'Sign In' : 'Create one'}
|
|
99
|
+
</a>
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
<div style={{ marginTop: '2rem', padding: '1rem', background: 'var(--bg-card)', borderRadius: '8px' }}>
|
|
103
|
+
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginBottom: '0.5rem' }}>Test Accounts (password: password123)</p>
|
|
104
|
+
<p style={{ fontSize: '0.8rem' }}>admin@example.com • mod@example.com • user@example.com</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useApi } from '../hooks/useApi';
|
|
3
|
+
|
|
4
|
+
export default function Permissions() {
|
|
5
|
+
const api = useApi();
|
|
6
|
+
const [permissions, setPermissions] = useState([]);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const [error, setError] = useState('');
|
|
9
|
+
const [newPerm, setNewPerm] = useState({ key: '', description: '', category: '' });
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
loadPermissions();
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const loadPermissions = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await api.get('/permissions');
|
|
18
|
+
setPermissions(res.permissions || []);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
setError(err.message);
|
|
21
|
+
} finally {
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const createPermission = async (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
try {
|
|
29
|
+
await api.post('/permissions', newPerm);
|
|
30
|
+
setNewPerm({ key: '', description: '', category: '' });
|
|
31
|
+
loadPermissions();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err.message);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const deletePermission = async (key) => {
|
|
38
|
+
if (!confirm(`Delete permission "${key}"?`)) return;
|
|
39
|
+
try {
|
|
40
|
+
await api.delete(`/permissions/${key}`);
|
|
41
|
+
loadPermissions();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setError(err.message);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Group by category
|
|
48
|
+
const grouped = permissions.reduce((acc, perm) => {
|
|
49
|
+
const cat = perm.category || 'Other';
|
|
50
|
+
if (!acc[cat]) acc[cat] = [];
|
|
51
|
+
acc[cat].push(perm);
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<div className="page-header">
|
|
58
|
+
<div>
|
|
59
|
+
<h1 className="page-title">Permissions</h1>
|
|
60
|
+
<p className="page-subtitle">Manage available permissions</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{error && <div className="message message-error">{error}</div>}
|
|
65
|
+
|
|
66
|
+
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
|
67
|
+
<div className="card-header">
|
|
68
|
+
<h3 className="card-title">Create Permission</h3>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="card-body">
|
|
71
|
+
<form onSubmit={createPermission} style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
|
72
|
+
<div style={{ flex: '2', minWidth: '200px' }}>
|
|
73
|
+
<label className="form-label">Key</label>
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
className="form-input"
|
|
77
|
+
placeholder="e.g. posts.create"
|
|
78
|
+
value={newPerm.key}
|
|
79
|
+
onChange={(e) => setNewPerm({ ...newPerm, key: e.target.value })}
|
|
80
|
+
required
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div style={{ flex: '2', minWidth: '200px' }}>
|
|
84
|
+
<label className="form-label">Description</label>
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
className="form-input"
|
|
88
|
+
placeholder="Optional description"
|
|
89
|
+
value={newPerm.description}
|
|
90
|
+
onChange={(e) => setNewPerm({ ...newPerm, description: e.target.value })}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div style={{ flex: '1', minWidth: '150px' }}>
|
|
94
|
+
<label className="form-label">Category</label>
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
className="form-input"
|
|
98
|
+
placeholder="e.g. posts"
|
|
99
|
+
value={newPerm.category}
|
|
100
|
+
onChange={(e) => setNewPerm({ ...newPerm, category: e.target.value })}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
|
104
|
+
<button type="submit" className="btn btn-primary">Create</button>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{loading ? (
|
|
111
|
+
<div className="loading"><div className="spinner"></div></div>
|
|
112
|
+
) : (
|
|
113
|
+
Object.entries(grouped).map(([category, perms]) => (
|
|
114
|
+
<div key={category} className="card" style={{ marginBottom: '1rem' }}>
|
|
115
|
+
<div className="card-header">
|
|
116
|
+
<h3 className="card-title">{category}</h3>
|
|
117
|
+
<span className="badge badge-primary">{perms.length} permissions</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="card-body">
|
|
120
|
+
<div className="permission-grid">
|
|
121
|
+
{perms.map(perm => (
|
|
122
|
+
<div key={perm.id} className="permission-item">
|
|
123
|
+
<div>
|
|
124
|
+
<div className="permission-key">{perm.key}</div>
|
|
125
|
+
<div className="permission-desc">{perm.description || 'No description'}</div>
|
|
126
|
+
</div>
|
|
127
|
+
<button
|
|
128
|
+
className="btn btn-danger btn-sm"
|
|
129
|
+
onClick={() => deletePermission(perm.key)}
|
|
130
|
+
>
|
|
131
|
+
✕
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
))
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{!loading && permissions.length === 0 && (
|
|
142
|
+
<div className="card">
|
|
143
|
+
<div className="card-body">
|
|
144
|
+
<p className="empty-state">No permissions yet. Create one above!</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useApi } from '../hooks/useApi';
|
|
3
|
+
|
|
4
|
+
export default function Roles() {
|
|
5
|
+
const api = useApi();
|
|
6
|
+
const [roles, setRoles] = useState([]);
|
|
7
|
+
const [permissions, setPermissions] = useState([]);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [selectedRole, setSelectedRole] = useState(null);
|
|
11
|
+
const [roleDetails, setRoleDetails] = useState(null);
|
|
12
|
+
|
|
13
|
+
// New role form
|
|
14
|
+
const [newRole, setNewRole] = useState({ name: '', description: '', priority: 0 });
|
|
15
|
+
const [newPermKey, setNewPermKey] = useState('');
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadData();
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (selectedRole) {
|
|
23
|
+
loadRoleDetails(selectedRole.id);
|
|
24
|
+
}
|
|
25
|
+
}, [selectedRole]);
|
|
26
|
+
|
|
27
|
+
const loadData = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const [rolesRes, permsRes] = await Promise.all([
|
|
30
|
+
api.get('/roles'),
|
|
31
|
+
api.get('/permissions'),
|
|
32
|
+
]);
|
|
33
|
+
setRoles(rolesRes.roles || []);
|
|
34
|
+
setPermissions(permsRes.permissions || []);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err.message);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const loadRoleDetails = async (roleId) => {
|
|
43
|
+
try {
|
|
44
|
+
const details = await api.get(`/roles/${roleId}`);
|
|
45
|
+
setRoleDetails(details);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setError(err.message);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createRole = async (e) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
try {
|
|
54
|
+
await api.post('/roles', newRole);
|
|
55
|
+
setNewRole({ name: '', description: '', priority: 0 });
|
|
56
|
+
loadData();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err.message);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const assignPermission = async () => {
|
|
63
|
+
if (!selectedRole || !newPermKey) return;
|
|
64
|
+
try {
|
|
65
|
+
await api.post(`/roles/${selectedRole.id}/permissions`, { permissionKey: newPermKey });
|
|
66
|
+
setNewPermKey('');
|
|
67
|
+
loadRoleDetails(selectedRole.id);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
setError(err.message);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const removePermission = async (permKey) => {
|
|
74
|
+
if (!selectedRole) return;
|
|
75
|
+
try {
|
|
76
|
+
await api.delete(`/roles/${selectedRole.id}/permissions/${permKey}`);
|
|
77
|
+
loadRoleDetails(selectedRole.id);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(err.message);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const deleteRole = async (roleId) => {
|
|
84
|
+
if (!confirm('Are you sure you want to delete this role?')) return;
|
|
85
|
+
try {
|
|
86
|
+
await api.delete(`/roles/${roleId}`);
|
|
87
|
+
setSelectedRole(null);
|
|
88
|
+
setRoleDetails(null);
|
|
89
|
+
loadData();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setError(err.message);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
<div className="page-header">
|
|
98
|
+
<div>
|
|
99
|
+
<h1 className="page-title">Roles</h1>
|
|
100
|
+
<p className="page-subtitle">Manage roles and their permissions</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{error && <div className="message message-error">{error}</div>}
|
|
105
|
+
|
|
106
|
+
{loading ? (
|
|
107
|
+
<div className="loading"><div className="spinner"></div></div>
|
|
108
|
+
) : (
|
|
109
|
+
<div className="grid grid-2">
|
|
110
|
+
<div>
|
|
111
|
+
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
|
112
|
+
<div className="card-header">
|
|
113
|
+
<h3 className="card-title">Create Role</h3>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="card-body">
|
|
116
|
+
<form onSubmit={createRole}>
|
|
117
|
+
<div className="form-group">
|
|
118
|
+
<label className="form-label">Name</label>
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
className="form-input"
|
|
122
|
+
placeholder="e.g. editor"
|
|
123
|
+
value={newRole.name}
|
|
124
|
+
onChange={(e) => setNewRole({ ...newRole, name: e.target.value })}
|
|
125
|
+
required
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="form-group">
|
|
129
|
+
<label className="form-label">Description</label>
|
|
130
|
+
<input
|
|
131
|
+
type="text"
|
|
132
|
+
className="form-input"
|
|
133
|
+
placeholder="Optional description"
|
|
134
|
+
value={newRole.description}
|
|
135
|
+
onChange={(e) => setNewRole({ ...newRole, description: e.target.value })}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="form-group">
|
|
139
|
+
<label className="form-label">Priority</label>
|
|
140
|
+
<input
|
|
141
|
+
type="number"
|
|
142
|
+
className="form-input"
|
|
143
|
+
value={newRole.priority}
|
|
144
|
+
onChange={(e) => setNewRole({ ...newRole, priority: parseInt(e.target.value) || 0 })}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<button type="submit" className="btn btn-primary">Create Role</button>
|
|
148
|
+
</form>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="card">
|
|
153
|
+
<div className="card-header">
|
|
154
|
+
<h3 className="card-title">All Roles</h3>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="card-body">
|
|
157
|
+
{roles.map(role => (
|
|
158
|
+
<div
|
|
159
|
+
key={role.id}
|
|
160
|
+
onClick={() => setSelectedRole(role)}
|
|
161
|
+
style={{
|
|
162
|
+
padding: '1rem',
|
|
163
|
+
marginBottom: '0.5rem',
|
|
164
|
+
background: selectedRole?.id === role.id ? 'var(--accent)' : 'var(--bg-card)',
|
|
165
|
+
borderRadius: '8px',
|
|
166
|
+
cursor: 'pointer',
|
|
167
|
+
display: 'flex',
|
|
168
|
+
justifyContent: 'space-between',
|
|
169
|
+
alignItems: 'center',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<div>
|
|
173
|
+
<strong>{role.name}</strong>
|
|
174
|
+
<span className="badge badge-primary" style={{ marginLeft: '0.5rem' }}>
|
|
175
|
+
Priority: {role.priority}
|
|
176
|
+
</span>
|
|
177
|
+
{role.isDefault && <span className="badge badge-success" style={{ marginLeft: '0.25rem' }}>Default</span>}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="card">
|
|
186
|
+
<div className="card-header">
|
|
187
|
+
<h3 className="card-title">
|
|
188
|
+
{selectedRole ? `Role: ${selectedRole.name}` : 'Select a role'}
|
|
189
|
+
</h3>
|
|
190
|
+
{selectedRole && (
|
|
191
|
+
<button className="btn btn-danger btn-sm" onClick={() => deleteRole(selectedRole.id)}>
|
|
192
|
+
Delete
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
<div className="card-body">
|
|
197
|
+
{selectedRole && roleDetails ? (
|
|
198
|
+
<>
|
|
199
|
+
<div className="form-group">
|
|
200
|
+
<label className="form-label">Assign Permission</label>
|
|
201
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
202
|
+
<select
|
|
203
|
+
className="form-input"
|
|
204
|
+
value={newPermKey}
|
|
205
|
+
onChange={(e) => setNewPermKey(e.target.value)}
|
|
206
|
+
>
|
|
207
|
+
<option value="">Select permission...</option>
|
|
208
|
+
<option value="*">* (All permissions)</option>
|
|
209
|
+
{permissions.map(perm => (
|
|
210
|
+
<option key={perm.id} value={perm.key}>{perm.key}</option>
|
|
211
|
+
))}
|
|
212
|
+
</select>
|
|
213
|
+
<button className="btn btn-primary" onClick={assignPermission}>Assign</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div style={{ marginTop: '1.5rem' }}>
|
|
218
|
+
<label className="form-label">Current Permissions</label>
|
|
219
|
+
{roleDetails.permissions?.length > 0 ? (
|
|
220
|
+
<div className="permission-grid">
|
|
221
|
+
{roleDetails.permissions.map(perm => (
|
|
222
|
+
<div key={perm.id} className="permission-item">
|
|
223
|
+
<div>
|
|
224
|
+
<div className="permission-key">{perm.key}</div>
|
|
225
|
+
<span className={`badge ${perm.granted ? 'badge-success' : 'badge-danger'}`}>
|
|
226
|
+
{perm.granted ? 'Granted' : 'Denied'}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
<button
|
|
230
|
+
className="btn btn-danger btn-sm"
|
|
231
|
+
onClick={() => removePermission(perm.key)}
|
|
232
|
+
>
|
|
233
|
+
✕
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
) : (
|
|
239
|
+
<p style={{ color: 'var(--text-secondary)' }}>No permissions assigned</p>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{roleDetails.inheritsFrom?.length > 0 && (
|
|
244
|
+
<div style={{ marginTop: '1.5rem' }}>
|
|
245
|
+
<label className="form-label">Inherits From</label>
|
|
246
|
+
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
247
|
+
{roleDetails.inheritsFrom.map(r => (
|
|
248
|
+
<span key={r.id} className="badge badge-primary">{r.name}</span>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</>
|
|
254
|
+
) : (
|
|
255
|
+
<p className="empty-state">Select a role from the list</p>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|