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.
@@ -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
+ }