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,462 @@
1
+ :root {
2
+ --bg-primary: #0f172a;
3
+ --bg-secondary: #1e293b;
4
+ --bg-card: #334155;
5
+ --text-primary: #f1f5f9;
6
+ --text-secondary: #94a3b8;
7
+ --accent: #6366f1;
8
+ --accent-hover: #818cf8;
9
+ --success: #22c55e;
10
+ --danger: #ef4444;
11
+ --warning: #f59e0b;
12
+ --border: #475569;
13
+ }
14
+
15
+ * {
16
+ margin: 0;
17
+ padding: 0;
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
23
+ background: var(--bg-primary);
24
+ color: var(--text-primary);
25
+ line-height: 1.6;
26
+ min-height: 100vh;
27
+ }
28
+
29
+ /* Layout */
30
+ .app {
31
+ display: flex;
32
+ min-height: 100vh;
33
+ }
34
+
35
+ .sidebar {
36
+ width: 260px;
37
+ background: var(--bg-secondary);
38
+ border-right: 1px solid var(--border);
39
+ padding: 1.5rem;
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+
44
+ .sidebar-logo {
45
+ font-size: 1.5rem;
46
+ font-weight: 700;
47
+ color: var(--accent);
48
+ margin-bottom: 2rem;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 0.5rem;
52
+ }
53
+
54
+ .sidebar-nav {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 0.5rem;
58
+ }
59
+
60
+ .nav-link {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0.75rem;
64
+ padding: 0.75rem 1rem;
65
+ color: var(--text-secondary);
66
+ text-decoration: none;
67
+ border-radius: 8px;
68
+ transition: all 0.2s;
69
+ }
70
+
71
+ .nav-link:hover, .nav-link.active {
72
+ background: var(--bg-card);
73
+ color: var(--text-primary);
74
+ }
75
+
76
+ .nav-link.active {
77
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
78
+ color: white;
79
+ }
80
+
81
+ .sidebar-user {
82
+ margin-top: auto;
83
+ padding-top: 1rem;
84
+ border-top: 1px solid var(--border);
85
+ }
86
+
87
+ .user-info {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.75rem;
91
+ margin-bottom: 0.75rem;
92
+ }
93
+
94
+ .user-avatar {
95
+ width: 40px;
96
+ height: 40px;
97
+ border-radius: 50%;
98
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ font-weight: 600;
103
+ font-size: 1rem;
104
+ }
105
+
106
+ .user-details {
107
+ flex: 1;
108
+ }
109
+
110
+ .user-name {
111
+ font-weight: 500;
112
+ font-size: 0.9rem;
113
+ }
114
+
115
+ .user-role {
116
+ font-size: 0.75rem;
117
+ color: var(--text-secondary);
118
+ }
119
+
120
+ .main-content {
121
+ flex: 1;
122
+ padding: 2rem;
123
+ overflow-y: auto;
124
+ }
125
+
126
+ /* Cards */
127
+ .card {
128
+ background: var(--bg-secondary);
129
+ border-radius: 12px;
130
+ border: 1px solid var(--border);
131
+ overflow: hidden;
132
+ }
133
+
134
+ .card-header {
135
+ padding: 1.25rem 1.5rem;
136
+ border-bottom: 1px solid var(--border);
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: space-between;
140
+ }
141
+
142
+ .card-title {
143
+ font-size: 1.1rem;
144
+ font-weight: 600;
145
+ }
146
+
147
+ .card-body {
148
+ padding: 1.5rem;
149
+ }
150
+
151
+ /* Buttons */
152
+ .btn {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ gap: 0.5rem;
157
+ padding: 0.625rem 1.25rem;
158
+ font-size: 0.875rem;
159
+ font-weight: 500;
160
+ border-radius: 8px;
161
+ border: none;
162
+ cursor: pointer;
163
+ transition: all 0.2s;
164
+ }
165
+
166
+ .btn-primary {
167
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
168
+ color: white;
169
+ }
170
+
171
+ .btn-primary:hover {
172
+ transform: translateY(-1px);
173
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
174
+ }
175
+
176
+ .btn-secondary {
177
+ background: var(--bg-card);
178
+ color: var(--text-primary);
179
+ }
180
+
181
+ .btn-secondary:hover {
182
+ background: var(--border);
183
+ }
184
+
185
+ .btn-danger {
186
+ background: var(--danger);
187
+ color: white;
188
+ }
189
+
190
+ .btn-danger:hover {
191
+ background: #dc2626;
192
+ }
193
+
194
+ .btn-sm {
195
+ padding: 0.375rem 0.75rem;
196
+ font-size: 0.8rem;
197
+ }
198
+
199
+ /* Forms */
200
+ .form-group {
201
+ margin-bottom: 1.25rem;
202
+ }
203
+
204
+ .form-label {
205
+ display: block;
206
+ margin-bottom: 0.5rem;
207
+ font-size: 0.875rem;
208
+ font-weight: 500;
209
+ color: var(--text-secondary);
210
+ }
211
+
212
+ .form-input {
213
+ width: 100%;
214
+ padding: 0.75rem 1rem;
215
+ font-size: 0.9rem;
216
+ background: var(--bg-card);
217
+ border: 1px solid var(--border);
218
+ border-radius: 8px;
219
+ color: var(--text-primary);
220
+ transition: border-color 0.2s;
221
+ }
222
+
223
+ .form-input:focus {
224
+ outline: none;
225
+ border-color: var(--accent);
226
+ }
227
+
228
+ .form-input::placeholder {
229
+ color: var(--text-secondary);
230
+ }
231
+
232
+ /* Tables */
233
+ .table-container {
234
+ overflow-x: auto;
235
+ }
236
+
237
+ table {
238
+ width: 100%;
239
+ border-collapse: collapse;
240
+ }
241
+
242
+ th, td {
243
+ padding: 1rem;
244
+ text-align: left;
245
+ border-bottom: 1px solid var(--border);
246
+ }
247
+
248
+ th {
249
+ font-weight: 600;
250
+ color: var(--text-secondary);
251
+ font-size: 0.8rem;
252
+ text-transform: uppercase;
253
+ letter-spacing: 0.05em;
254
+ }
255
+
256
+ tr:hover {
257
+ background: var(--bg-card);
258
+ }
259
+
260
+ /* Badges */
261
+ .badge {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ padding: 0.25rem 0.625rem;
265
+ font-size: 0.75rem;
266
+ font-weight: 500;
267
+ border-radius: 9999px;
268
+ }
269
+
270
+ .badge-primary {
271
+ background: rgba(99, 102, 241, 0.2);
272
+ color: var(--accent-hover);
273
+ }
274
+
275
+ .badge-success {
276
+ background: rgba(34, 197, 94, 0.2);
277
+ color: var(--success);
278
+ }
279
+
280
+ .badge-danger {
281
+ background: rgba(239, 68, 68, 0.2);
282
+ color: var(--danger);
283
+ }
284
+
285
+ .badge-warning {
286
+ background: rgba(245, 158, 11, 0.2);
287
+ color: var(--warning);
288
+ }
289
+
290
+ /* Grid */
291
+ .grid {
292
+ display: grid;
293
+ gap: 1.5rem;
294
+ }
295
+
296
+ .grid-2 {
297
+ grid-template-columns: repeat(2, 1fr);
298
+ }
299
+
300
+ .grid-3 {
301
+ grid-template-columns: repeat(3, 1fr);
302
+ }
303
+
304
+ .grid-4 {
305
+ grid-template-columns: repeat(4, 1fr);
306
+ }
307
+
308
+ /* Stats */
309
+ .stat-card {
310
+ background: var(--bg-secondary);
311
+ border-radius: 12px;
312
+ padding: 1.5rem;
313
+ border: 1px solid var(--border);
314
+ }
315
+
316
+ .stat-value {
317
+ font-size: 2rem;
318
+ font-weight: 700;
319
+ margin-bottom: 0.25rem;
320
+ }
321
+
322
+ .stat-label {
323
+ font-size: 0.875rem;
324
+ color: var(--text-secondary);
325
+ }
326
+
327
+ /* Auth page */
328
+ .auth-container {
329
+ min-height: 100vh;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ background: linear-gradient(135deg, var(--bg-primary) 0%, #1a1a2e 100%);
334
+ }
335
+
336
+ .auth-card {
337
+ width: 100%;
338
+ max-width: 420px;
339
+ padding: 2.5rem;
340
+ background: var(--bg-secondary);
341
+ border-radius: 16px;
342
+ border: 1px solid var(--border);
343
+ }
344
+
345
+ .auth-title {
346
+ font-size: 1.75rem;
347
+ font-weight: 700;
348
+ text-align: center;
349
+ margin-bottom: 0.5rem;
350
+ }
351
+
352
+ .auth-subtitle {
353
+ text-align: center;
354
+ color: var(--text-secondary);
355
+ margin-bottom: 2rem;
356
+ }
357
+
358
+ .auth-switch {
359
+ text-align: center;
360
+ margin-top: 1.5rem;
361
+ color: var(--text-secondary);
362
+ }
363
+
364
+ .auth-switch a {
365
+ color: var(--accent);
366
+ text-decoration: none;
367
+ }
368
+
369
+ .auth-switch a:hover {
370
+ text-decoration: underline;
371
+ }
372
+
373
+ /* Messages */
374
+ .message {
375
+ padding: 1rem;
376
+ border-radius: 8px;
377
+ margin-bottom: 1rem;
378
+ }
379
+
380
+ .message-error {
381
+ background: rgba(239, 68, 68, 0.1);
382
+ border: 1px solid var(--danger);
383
+ color: var(--danger);
384
+ }
385
+
386
+ .message-success {
387
+ background: rgba(34, 197, 94, 0.1);
388
+ border: 1px solid var(--success);
389
+ color: var(--success);
390
+ }
391
+
392
+ /* Permission grid */
393
+ .permission-grid {
394
+ display: grid;
395
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
396
+ gap: 1rem;
397
+ }
398
+
399
+ .permission-item {
400
+ padding: 1rem;
401
+ background: var(--bg-card);
402
+ border-radius: 8px;
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: space-between;
406
+ }
407
+
408
+ .permission-key {
409
+ font-weight: 500;
410
+ font-size: 0.9rem;
411
+ }
412
+
413
+ .permission-desc {
414
+ font-size: 0.8rem;
415
+ color: var(--text-secondary);
416
+ }
417
+
418
+ /* Empty state */
419
+ .empty-state {
420
+ text-align: center;
421
+ padding: 3rem;
422
+ color: var(--text-secondary);
423
+ }
424
+
425
+ /* Loading */
426
+ .loading {
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ padding: 3rem;
431
+ }
432
+
433
+ .spinner {
434
+ width: 40px;
435
+ height: 40px;
436
+ border: 3px solid var(--border);
437
+ border-top-color: var(--accent);
438
+ border-radius: 50%;
439
+ animation: spin 0.8s linear infinite;
440
+ }
441
+
442
+ @keyframes spin {
443
+ to { transform: rotate(360deg); }
444
+ }
445
+
446
+ /* Page header */
447
+ .page-header {
448
+ display: flex;
449
+ align-items: center;
450
+ justify-content: space-between;
451
+ margin-bottom: 2rem;
452
+ }
453
+
454
+ .page-title {
455
+ font-size: 1.75rem;
456
+ font-weight: 700;
457
+ }
458
+
459
+ .page-subtitle {
460
+ color: var(--text-secondary);
461
+ margin-top: 0.25rem;
462
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -0,0 +1,148 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+ import { useApi } from '../hooks/useApi';
4
+
5
+ export default function Dashboard() {
6
+ const { user } = useAuth();
7
+ const api = useApi();
8
+ const [stats, setStats] = useState({ users: 0, roles: 0, permissions: 0 });
9
+ const [loading, setLoading] = useState(true);
10
+ const [permissionCheck, setPermissionCheck] = useState({ key: '', result: null });
11
+
12
+ useEffect(() => {
13
+ loadStats();
14
+ }, []);
15
+
16
+ const loadStats = async () => {
17
+ try {
18
+ const [usersRes, rolesRes, permsRes] = await Promise.allSettled([
19
+ api.get('/users'),
20
+ api.get('/roles'),
21
+ api.get('/permissions'),
22
+ ]);
23
+
24
+ setStats({
25
+ users: usersRes.status === 'fulfilled' ? usersRes.value.users?.length || 0 : '?',
26
+ roles: rolesRes.status === 'fulfilled' ? rolesRes.value.roles?.length || 0 : '?',
27
+ permissions: permsRes.status === 'fulfilled' ? permsRes.value.permissions?.length || 0 : '?',
28
+ });
29
+ } catch (error) {
30
+ console.error('Failed to load stats:', error);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ const checkPermission = async () => {
37
+ if (!permissionCheck.key) return;
38
+ try {
39
+ const result = await api.get(`/users/${user.id}/check/${permissionCheck.key}`);
40
+ setPermissionCheck(prev => ({ ...prev, result: result.hasPermission }));
41
+ } catch (error) {
42
+ setPermissionCheck(prev => ({ ...prev, result: 'error' }));
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div>
48
+ <div className="page-header">
49
+ <div>
50
+ <h1 className="page-title">Dashboard</h1>
51
+ <p className="page-subtitle">Welcome back, {user?.name || user?.email}</p>
52
+ </div>
53
+ </div>
54
+
55
+ {loading ? (
56
+ <div className="loading"><div className="spinner"></div></div>
57
+ ) : (
58
+ <>
59
+ <div className="grid grid-3" style={{ marginBottom: '2rem' }}>
60
+ <div className="stat-card">
61
+ <div className="stat-value">{stats.users}</div>
62
+ <div className="stat-label">Users</div>
63
+ </div>
64
+ <div className="stat-card">
65
+ <div className="stat-value">{stats.roles}</div>
66
+ <div className="stat-label">Roles</div>
67
+ </div>
68
+ <div className="stat-card">
69
+ <div className="stat-value">{stats.permissions}</div>
70
+ <div className="stat-label">Permissions</div>
71
+ </div>
72
+ </div>
73
+
74
+ <div className="grid grid-2">
75
+ <div className="card">
76
+ <div className="card-header">
77
+ <h3 className="card-title">Your Roles</h3>
78
+ </div>
79
+ <div className="card-body">
80
+ {user?.roles?.length > 0 ? (
81
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
82
+ {user.roles.map(role => (
83
+ <span key={role.id} className="badge badge-primary">
84
+ {role.name} (priority: {role.priority})
85
+ </span>
86
+ ))}
87
+ </div>
88
+ ) : (
89
+ <p className="empty-state">No roles assigned</p>
90
+ )}
91
+ </div>
92
+ </div>
93
+
94
+ <div className="card">
95
+ <div className="card-header">
96
+ <h3 className="card-title">Test Permission</h3>
97
+ </div>
98
+ <div className="card-body">
99
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
100
+ <input
101
+ type="text"
102
+ className="form-input"
103
+ placeholder="e.g. users.list"
104
+ value={permissionCheck.key}
105
+ onChange={(e) => setPermissionCheck({ key: e.target.value, result: null })}
106
+ />
107
+ <button className="btn btn-primary" onClick={checkPermission}>Check</button>
108
+ </div>
109
+ {permissionCheck.result !== null && (
110
+ <div style={{ marginTop: '1rem' }}>
111
+ <span className={`badge ${permissionCheck.result === true ? 'badge-success' : permissionCheck.result === 'error' ? 'badge-warning' : 'badge-danger'}`}>
112
+ {permissionCheck.result === true ? '✓ Allowed' : permissionCheck.result === 'error' ? 'Error' : '✗ Denied'}
113
+ </span>
114
+ </div>
115
+ )}
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="card" style={{ marginTop: '1.5rem' }}>
121
+ <div className="card-header">
122
+ <h3 className="card-title">Your Direct Permissions</h3>
123
+ </div>
124
+ <div className="card-body">
125
+ {user?.permissions?.direct?.length > 0 ? (
126
+ <div className="permission-grid">
127
+ {user.permissions.direct.map(perm => (
128
+ <div key={perm.id} className="permission-item">
129
+ <div>
130
+ <div className="permission-key">{perm.key}</div>
131
+ <div className="permission-desc">{perm.description || 'No description'}</div>
132
+ </div>
133
+ <span className={`badge ${perm.granted ? 'badge-success' : 'badge-danger'}`}>
134
+ {perm.granted ? 'Granted' : 'Denied'}
135
+ </span>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ ) : (
140
+ <p className="empty-state">No direct permissions assigned</p>
141
+ )}
142
+ </div>
143
+ </div>
144
+ </>
145
+ )}
146
+ </div>
147
+ );
148
+ }