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,171 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useApi } from '../hooks/useApi';
3
+
4
+ export default function Users() {
5
+ const api = useApi();
6
+ const [users, setUsers] = useState([]);
7
+ const [roles, setRoles] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+ const [error, setError] = useState('');
10
+ const [selectedUser, setSelectedUser] = useState(null);
11
+ const [assignRoleId, setAssignRoleId] = useState('');
12
+
13
+ useEffect(() => {
14
+ loadData();
15
+ }, []);
16
+
17
+ const loadData = async () => {
18
+ try {
19
+ const [usersRes, rolesRes] = await Promise.all([
20
+ api.get('/users'),
21
+ api.get('/roles'),
22
+ ]);
23
+ setUsers(usersRes.users || []);
24
+ setRoles(rolesRes.roles || []);
25
+ } catch (err) {
26
+ setError(err.message);
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ const assignRole = async () => {
33
+ if (!selectedUser || !assignRoleId) return;
34
+ try {
35
+ await api.post(`/users/${selectedUser.id}/roles`, { roleId: assignRoleId });
36
+ setAssignRoleId('');
37
+ loadData();
38
+ } catch (err) {
39
+ setError(err.message);
40
+ }
41
+ };
42
+
43
+ const removeRole = async (userId, roleId) => {
44
+ try {
45
+ await api.delete(`/users/${userId}/roles/${roleId}`);
46
+ loadData();
47
+ } catch (err) {
48
+ setError(err.message);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <div>
54
+ <div className="page-header">
55
+ <div>
56
+ <h1 className="page-title">Users</h1>
57
+ <p className="page-subtitle">Manage users and their roles</p>
58
+ </div>
59
+ </div>
60
+
61
+ {error && <div className="message message-error">{error}</div>}
62
+
63
+ {loading ? (
64
+ <div className="loading"><div className="spinner"></div></div>
65
+ ) : (
66
+ <div className="grid grid-2">
67
+ <div className="card">
68
+ <div className="card-header">
69
+ <h3 className="card-title">All Users</h3>
70
+ </div>
71
+ <div className="card-body">
72
+ <div className="table-container">
73
+ <table>
74
+ <thead>
75
+ <tr>
76
+ <th>Name</th>
77
+ <th>Email</th>
78
+ <th>Roles</th>
79
+ <th>Actions</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {users.map(user => (
84
+ <tr key={user.id}>
85
+ <td>{user.name || '-'}</td>
86
+ <td>{user.email}</td>
87
+ <td>
88
+ {user.roles?.map(role => (
89
+ <span key={role} className="badge badge-primary" style={{ marginRight: '0.25rem' }}>
90
+ {role}
91
+ </span>
92
+ ))}
93
+ </td>
94
+ <td>
95
+ <button
96
+ className="btn btn-secondary btn-sm"
97
+ onClick={() => setSelectedUser(user)}
98
+ >
99
+ Manage
100
+ </button>
101
+ </td>
102
+ </tr>
103
+ ))}
104
+ </tbody>
105
+ </table>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div className="card">
111
+ <div className="card-header">
112
+ <h3 className="card-title">
113
+ {selectedUser ? `Manage: ${selectedUser.name || selectedUser.email}` : 'Select a user'}
114
+ </h3>
115
+ </div>
116
+ <div className="card-body">
117
+ {selectedUser ? (
118
+ <>
119
+ <div className="form-group">
120
+ <label className="form-label">Assign Role</label>
121
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
122
+ <select
123
+ className="form-input"
124
+ value={assignRoleId}
125
+ onChange={(e) => setAssignRoleId(e.target.value)}
126
+ >
127
+ <option value="">Select role...</option>
128
+ {roles.map(role => (
129
+ <option key={role.id} value={role.id}>{role.name}</option>
130
+ ))}
131
+ </select>
132
+ <button className="btn btn-primary" onClick={assignRole}>Assign</button>
133
+ </div>
134
+ </div>
135
+
136
+ <div style={{ marginTop: '1.5rem' }}>
137
+ <label className="form-label">Current Roles</label>
138
+ {selectedUser.roles?.length > 0 ? (
139
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
140
+ {selectedUser.roles.map(roleName => {
141
+ const role = roles.find(r => r.name === roleName);
142
+ return role ? (
143
+ <div key={role.id} className="badge badge-primary" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
144
+ {roleName}
145
+ <button
146
+ onClick={() => removeRole(selectedUser.id, role.id)}
147
+ style={{ background: 'none', border: 'none', color: 'inherit', cursor: 'pointer' }}
148
+ >
149
+
150
+ </button>
151
+ </div>
152
+ ) : (
153
+ <span key={roleName} className="badge badge-primary">{roleName}</span>
154
+ );
155
+ })}
156
+ </div>
157
+ ) : (
158
+ <p style={{ color: 'var(--text-secondary)' }}>No roles assigned</p>
159
+ )}
160
+ </div>
161
+ </>
162
+ ) : (
163
+ <p className="empty-state">Select a user from the list to manage their roles</p>
164
+ )}
165
+ </div>
166
+ </div>
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ '/api': {
10
+ target: 'http://localhost:3001',
11
+ changeOrigin: true,
12
+ },
13
+ },
14
+ },
15
+ })
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "vperms-testing",
3
+ "version": "1.0.0",
4
+ "description": "Test project for @faryzal2020/v-perms package",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun run --watch server/index.js",
8
+ "dev:frontend": "cd frontend && bun run dev",
9
+ "db:push": "bunx prisma db push",
10
+ "db:generate": "bunx prisma generate",
11
+ "db:studio": "bunx prisma studio",
12
+ "db:seed": "bun run server/seed.js"
13
+ },
14
+ "dependencies": {
15
+ "@prisma/client": "^5.22.0",
16
+ "hono": "^4.6.0",
17
+ "@hono/node-server": "^1.13.0",
18
+ "@faryzal2020/v-perms": "file:../v-perms",
19
+ "bcryptjs": "^2.4.3",
20
+ "jsonwebtoken": "^9.0.2"
21
+ },
22
+ "devDependencies": {
23
+ "prisma": "^5.22.0"
24
+ }
25
+ }
@@ -0,0 +1,104 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(cuid())
12
+ email String @unique
13
+ name String?
14
+ password String
15
+ createdAt DateTime @default(now())
16
+ updatedAt DateTime @updatedAt
17
+
18
+ userRoles UserRole[]
19
+ userPermissions UserPermission[]
20
+
21
+ @@map("users")
22
+ }
23
+
24
+ model Role {
25
+ id String @id @default(cuid())
26
+ name String @unique
27
+ description String?
28
+ priority Int @default(0)
29
+ isDefault Boolean @default(false)
30
+ createdAt DateTime @default(now())
31
+ updatedAt DateTime @updatedAt
32
+
33
+ userRoles UserRole[]
34
+ rolePermissions RolePermission[]
35
+ inheritedRoles RoleInheritance[] @relation("ParentRole")
36
+ inheritsFrom RoleInheritance[] @relation("ChildRole")
37
+
38
+ @@map("roles")
39
+ }
40
+
41
+ model Permission {
42
+ id String @id @default(cuid())
43
+ key String @unique
44
+ description String?
45
+ category String?
46
+ createdAt DateTime @default(now())
47
+
48
+ rolePermissions RolePermission[]
49
+ userPermissions UserPermission[]
50
+
51
+ @@index([category])
52
+ @@map("permissions")
53
+ }
54
+
55
+ model UserRole {
56
+ userId String
57
+ roleId String
58
+ assignedAt DateTime @default(now())
59
+
60
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
61
+ role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
62
+
63
+ @@id([userId, roleId])
64
+ @@map("user_roles")
65
+ }
66
+
67
+ model RolePermission {
68
+ roleId String
69
+ permissionId String
70
+ granted Boolean @default(true)
71
+ assignedAt DateTime @default(now())
72
+
73
+ role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
74
+ permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
75
+
76
+ @@id([roleId, permissionId])
77
+ @@map("role_permissions")
78
+ }
79
+
80
+ model UserPermission {
81
+ userId String
82
+ permissionId String
83
+ granted Boolean @default(true)
84
+ assignedAt DateTime @default(now())
85
+
86
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
87
+ permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
88
+
89
+ @@id([userId, permissionId])
90
+ @@map("user_permissions")
91
+ }
92
+
93
+ model RoleInheritance {
94
+ roleId String
95
+ inheritsFromId String
96
+ priority Int @default(0)
97
+ createdAt DateTime @default(now())
98
+
99
+ role Role @relation("ParentRole", fields: [roleId], references: [id], onDelete: Cascade)
100
+ inheritsFrom Role @relation("ChildRole", fields: [inheritsFromId], references: [id], onDelete: Cascade)
101
+
102
+ @@id([roleId, inheritsFromId])
103
+ @@map("role_inheritance")
104
+ }
package/query ADDED
File without changes
@@ -0,0 +1,57 @@
1
+ import { Hono } from 'hono';
2
+ import { cors } from 'hono/cors';
3
+ import { serve } from '@hono/node-server';
4
+ import { PrismaClient } from '@prisma/client';
5
+ import { createPermissionSystem } from '@faryzal2020/v-perms';
6
+ import authRoutes from './routes/auth.js';
7
+ import userRoutes from './routes/users.js';
8
+ import roleRoutes from './routes/roles.js';
9
+ import permissionRoutes from './routes/permissions.js';
10
+
11
+ // Initialize Prisma
12
+ const prisma = new PrismaClient();
13
+
14
+ // Initialize Permission System
15
+ const perms = createPermissionSystem(prisma, {
16
+ debug: true,
17
+ enableCache: false, // No Redis for this demo
18
+ });
19
+
20
+ // Create Hono app
21
+ const app = new Hono();
22
+
23
+ // Middleware
24
+ app.use('*', cors({
25
+ origin: 'http://localhost:5173',
26
+ credentials: true,
27
+ }));
28
+
29
+ // Make prisma and perms available in context
30
+ app.use('*', async (c, next) => {
31
+ c.set('prisma', prisma);
32
+ c.set('perms', perms);
33
+ await next();
34
+ });
35
+
36
+ // Health check
37
+ app.get('/', (c) => {
38
+ return c.json({ message: 'v-perms Test API', status: 'running' });
39
+ });
40
+
41
+ // Routes
42
+ app.route('/api/auth', authRoutes);
43
+ app.route('/api/users', userRoutes);
44
+ app.route('/api/roles', roleRoutes);
45
+ app.route('/api/permissions', permissionRoutes);
46
+
47
+ // Start server
48
+ const port = process.env.PORT || 3001;
49
+
50
+ console.log(`🚀 Server starting on port ${port}`);
51
+
52
+ serve({
53
+ fetch: app.fetch,
54
+ port: Number(port),
55
+ });
56
+
57
+ console.log(`✅ Server running at http://localhost:${port}`);
@@ -0,0 +1,65 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-dev-key';
4
+
5
+ /**
6
+ * Middleware to verify JWT token
7
+ */
8
+ export async function authMiddleware(c, next) {
9
+ const authHeader = c.req.header('Authorization');
10
+
11
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
12
+ return c.json({ error: 'No token provided' }, 401);
13
+ }
14
+
15
+ const token = authHeader.split(' ')[1];
16
+
17
+ try {
18
+ const decoded = jwt.verify(token, JWT_SECRET);
19
+ c.set('user', decoded);
20
+ await next();
21
+ } catch (error) {
22
+ return c.json({ error: 'Invalid token' }, 401);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Middleware to check if user has permission
28
+ */
29
+ export function requirePermission(permission) {
30
+ return async (c, next) => {
31
+ const user = c.get('user');
32
+ const perms = c.get('perms');
33
+
34
+ if (!user) {
35
+ return c.json({ error: 'Unauthorized' }, 401);
36
+ }
37
+
38
+ try {
39
+ const hasPermission = await perms.can(user.id, permission);
40
+
41
+ if (!hasPermission) {
42
+ return c.json({
43
+ error: 'Forbidden',
44
+ required: permission
45
+ }, 403);
46
+ }
47
+
48
+ await next();
49
+ } catch (error) {
50
+ console.error('Permission check error:', error);
51
+ return c.json({ error: 'Permission check failed' }, 500);
52
+ }
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Generate JWT token
58
+ */
59
+ export function generateToken(user) {
60
+ return jwt.sign(
61
+ { id: user.id, email: user.email },
62
+ JWT_SECRET,
63
+ { expiresIn: '24h' }
64
+ );
65
+ }
@@ -0,0 +1,157 @@
1
+ import { Hono } from 'hono';
2
+ import bcrypt from 'bcryptjs';
3
+ import { generateToken, authMiddleware } from '../middleware/auth.js';
4
+
5
+ const router = new Hono();
6
+
7
+ /**
8
+ * POST /api/auth/register
9
+ * Register a new user
10
+ */
11
+ router.post('/register', async (c) => {
12
+ const prisma = c.get('prisma');
13
+ const perms = c.get('perms');
14
+
15
+ try {
16
+ const { email, password, name } = await c.req.json();
17
+
18
+ // Validate input
19
+ if (!email || !password) {
20
+ return c.json({ error: 'Email and password required' }, 400);
21
+ }
22
+
23
+ // Check if user exists
24
+ const existing = await prisma.user.findUnique({
25
+ where: { email },
26
+ });
27
+
28
+ if (existing) {
29
+ return c.json({ error: 'Email already registered' }, 400);
30
+ }
31
+
32
+ // Hash password
33
+ const hashedPassword = await bcrypt.hash(password, 10);
34
+
35
+ // Create user
36
+ const user = await prisma.user.create({
37
+ data: {
38
+ email,
39
+ password: hashedPassword,
40
+ name: name || null,
41
+ },
42
+ });
43
+
44
+ // Assign default role if exists
45
+ try {
46
+ const defaultRole = await prisma.role.findFirst({
47
+ where: { isDefault: true },
48
+ });
49
+
50
+ if (defaultRole) {
51
+ await perms.assignRole(defaultRole.id, user.id);
52
+ }
53
+ } catch (roleError) {
54
+ console.log('No default role to assign');
55
+ }
56
+
57
+ // Generate token
58
+ const token = generateToken(user);
59
+
60
+ return c.json({
61
+ message: 'Registration successful',
62
+ user: { id: user.id, email: user.email, name: user.name },
63
+ token,
64
+ });
65
+ } catch (error) {
66
+ console.error('Registration error:', error);
67
+ return c.json({ error: 'Registration failed' }, 500);
68
+ }
69
+ });
70
+
71
+ /**
72
+ * POST /api/auth/login
73
+ * Login user
74
+ */
75
+ router.post('/login', async (c) => {
76
+ const prisma = c.get('prisma');
77
+
78
+ try {
79
+ const { email, password } = await c.req.json();
80
+
81
+ // Validate input
82
+ if (!email || !password) {
83
+ return c.json({ error: 'Email and password required' }, 400);
84
+ }
85
+
86
+ // Find user
87
+ const user = await prisma.user.findUnique({
88
+ where: { email },
89
+ });
90
+
91
+ if (!user) {
92
+ return c.json({ error: 'Invalid credentials' }, 401);
93
+ }
94
+
95
+ // Check password
96
+ const validPassword = await bcrypt.compare(password, user.password);
97
+
98
+ if (!validPassword) {
99
+ return c.json({ error: 'Invalid credentials' }, 401);
100
+ }
101
+
102
+ // Generate token
103
+ const token = generateToken(user);
104
+
105
+ return c.json({
106
+ message: 'Login successful',
107
+ user: { id: user.id, email: user.email, name: user.name },
108
+ token,
109
+ });
110
+ } catch (error) {
111
+ console.error('Login error:', error);
112
+ return c.json({ error: 'Login failed' }, 500);
113
+ }
114
+ });
115
+
116
+ /**
117
+ * GET /api/auth/me
118
+ * Get current user info with roles and permissions
119
+ */
120
+ router.get('/me', authMiddleware, async (c) => {
121
+ const prisma = c.get('prisma');
122
+ const perms = c.get('perms');
123
+ const authUser = c.get('user');
124
+
125
+ try {
126
+ const user = await prisma.user.findUnique({
127
+ where: { id: authUser.id },
128
+ select: {
129
+ id: true,
130
+ email: true,
131
+ name: true,
132
+ createdAt: true,
133
+ },
134
+ });
135
+
136
+ if (!user) {
137
+ return c.json({ error: 'User not found' }, 404);
138
+ }
139
+
140
+ // Get user's roles
141
+ const roles = await perms.manager.getUserRoles(user.id);
142
+
143
+ // Get user's permissions
144
+ const permissions = await perms.manager.getUserPermissions(user.id);
145
+
146
+ return c.json({
147
+ user,
148
+ roles: roles.map(r => ({ id: r.id, name: r.name, priority: r.priority })),
149
+ permissions,
150
+ });
151
+ } catch (error) {
152
+ console.error('Get user error:', error);
153
+ return c.json({ error: 'Failed to get user' }, 500);
154
+ }
155
+ });
156
+
157
+ export default router;
@@ -0,0 +1,64 @@
1
+ import { Hono } from 'hono';
2
+ import { authMiddleware, requirePermission } from '../middleware/auth.js';
3
+
4
+ const router = new Hono();
5
+
6
+ // All routes require authentication
7
+ router.use('*', authMiddleware);
8
+
9
+ /**
10
+ * GET /api/permissions
11
+ * List all permissions
12
+ */
13
+ router.get('/', requirePermission('permissions.list'), async (c) => {
14
+ const perms = c.get('perms');
15
+
16
+ try {
17
+ const permissions = await perms.manager.listPermissions();
18
+ return c.json({ permissions });
19
+ } catch (error) {
20
+ console.error('List permissions error:', error);
21
+ return c.json({ error: 'Failed to list permissions' }, 500);
22
+ }
23
+ });
24
+
25
+ /**
26
+ * POST /api/permissions
27
+ * Create a new permission
28
+ */
29
+ router.post('/', requirePermission('permissions.create'), async (c) => {
30
+ const perms = c.get('perms');
31
+
32
+ try {
33
+ const { key, description, category } = await c.req.json();
34
+
35
+ if (!key) {
36
+ return c.json({ error: 'Permission key required' }, 400);
37
+ }
38
+
39
+ const permission = await perms.createPermission(key, description, category);
40
+ return c.json({ message: 'Permission created', permission });
41
+ } catch (error) {
42
+ console.error('Create permission error:', error);
43
+ return c.json({ error: error.message || 'Failed to create permission' }, 500);
44
+ }
45
+ });
46
+
47
+ /**
48
+ * DELETE /api/permissions/:key
49
+ * Delete a permission
50
+ */
51
+ router.delete('/:key', requirePermission('permissions.delete'), async (c) => {
52
+ const perms = c.get('perms');
53
+ const key = c.req.param('key');
54
+
55
+ try {
56
+ await perms.deletePermission(key);
57
+ return c.json({ message: 'Permission deleted' });
58
+ } catch (error) {
59
+ console.error('Delete permission error:', error);
60
+ return c.json({ error: error.message || 'Failed to delete permission' }, 500);
61
+ }
62
+ });
63
+
64
+ export default router;