mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-06-23 15:48:08 +02:00
Add user management features to admin section
- Add ability to create new users from admin panel - Add role dropdown (User/Admin) for each user - Replace toggle buttons with select dropdown for role management - Admin users can access the Admin section in settings - Regular users see only Profile and Notifications sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f46c6ad9d4
commit
040cdb9c42
3 changed files with 200 additions and 25 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { Router, Response, NextFunction } from 'express';
|
import { Router, Response, NextFunction } from 'express';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
import { AuthRequest, authMiddleware } from '../middleware/auth';
|
import { AuthRequest, authMiddleware } from '../middleware/auth';
|
||||||
import { userQueries, systemSettingsQueries } from '../models';
|
import { userQueries, systemSettingsQueries } from '../models';
|
||||||
|
|
||||||
|
|
@ -38,6 +39,57 @@ router.get('/users', async (_req: AuthRequest, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a new user
|
||||||
|
router.post('/users', async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email, password, is_admin } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
res.status(400).json({ error: 'Email and password are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
res.status(400).json({ error: 'Invalid email format' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await userQueries.findByEmail(email);
|
||||||
|
if (existingUser) {
|
||||||
|
res.status(409).json({ error: 'Email already registered' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saltRounds = 12;
|
||||||
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
|
const user = await userQueries.create(email, passwordHash);
|
||||||
|
|
||||||
|
// Set admin status if specified
|
||||||
|
if (is_admin) {
|
||||||
|
await userQueries.setAdmin(user.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'User created successfully',
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
is_admin: is_admin || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create user' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Delete a user
|
// Delete a user
|
||||||
router.delete('/users/:id', async (req: AuthRequest, res: Response) => {
|
router.delete('/users/:id', async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,13 @@ export interface SystemSettings {
|
||||||
export const adminApi = {
|
export const adminApi = {
|
||||||
getUsers: () => api.get<UserProfile[]>('/admin/users'),
|
getUsers: () => api.get<UserProfile[]>('/admin/users'),
|
||||||
|
|
||||||
|
createUser: (email: string, password: string, isAdmin: boolean) =>
|
||||||
|
api.post<{ message: string; user: UserProfile }>('/admin/users', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
is_admin: isAdmin,
|
||||||
|
}),
|
||||||
|
|
||||||
deleteUser: (id: number) => api.delete(`/admin/users/${id}`),
|
deleteUser: (id: number) => api.delete(`/admin/users/${id}`),
|
||||||
|
|
||||||
setUserAdmin: (id: number, isAdmin: boolean) =>
|
setUserAdmin: (id: number, isAdmin: boolean) =>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ export default function Settings() {
|
||||||
const [systemSettings, setSystemSettings] = useState<SystemSettings | null>(null);
|
const [systemSettings, setSystemSettings] = useState<SystemSettings | null>(null);
|
||||||
const [isLoadingAdmin, setIsLoadingAdmin] = useState(false);
|
const [isLoadingAdmin, setIsLoadingAdmin] = useState(false);
|
||||||
const [isSavingAdmin, setIsSavingAdmin] = useState(false);
|
const [isSavingAdmin, setIsSavingAdmin] = useState(false);
|
||||||
|
const [showAddUser, setShowAddUser] = useState(false);
|
||||||
|
const [newUserEmail, setNewUserEmail] = useState('');
|
||||||
|
const [newUserPassword, setNewUserPassword] = useState('');
|
||||||
|
const [newUserRole, setNewUserRole] = useState<'user' | 'admin'>('user');
|
||||||
|
const [isCreatingUser, setIsCreatingUser] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
|
@ -210,6 +215,35 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!newUserEmail || !newUserPassword) {
|
||||||
|
setError('Email and password are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newUserPassword.length < 8) {
|
||||||
|
setError('Password must be at least 8 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCreatingUser(true);
|
||||||
|
try {
|
||||||
|
await adminApi.createUser(newUserEmail, newUserPassword, newUserRole === 'admin');
|
||||||
|
// Refresh users list
|
||||||
|
const usersRes = await adminApi.getUsers();
|
||||||
|
setUsers(usersRes.data);
|
||||||
|
setNewUserEmail('');
|
||||||
|
setNewUserPassword('');
|
||||||
|
setNewUserRole('user');
|
||||||
|
setShowAddUser(false);
|
||||||
|
setSuccess('User created successfully');
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(error.response?.data?.error || 'Failed to create user');
|
||||||
|
} finally {
|
||||||
|
setIsCreatingUser(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteUser = async (userId: number) => {
|
const handleDeleteUser = async (userId: number) => {
|
||||||
if (!confirm('Are you sure you want to delete this user? All their data will be lost.')) {
|
if (!confirm('Are you sure you want to delete this user? All their data will be lost.')) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -224,14 +258,15 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAdmin = async (userId: number, currentStatus: boolean) => {
|
const handleRoleChange = async (userId: number, newRole: 'user' | 'admin') => {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
|
const isAdmin = newRole === 'admin';
|
||||||
try {
|
try {
|
||||||
await adminApi.setUserAdmin(userId, !currentStatus);
|
await adminApi.setUserAdmin(userId, isAdmin);
|
||||||
setUsers(users.map(u => u.id === userId ? { ...u, is_admin: !currentStatus } : u));
|
setUsers(users.map(u => u.id === userId ? { ...u, is_admin: isAdmin } : u));
|
||||||
setSuccess(`Admin status ${!currentStatus ? 'granted' : 'revoked'}`);
|
setSuccess(`User role updated to ${newRole}`);
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to update admin status');
|
setError('Failed to update user role');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -872,6 +907,82 @@ export default function Settings() {
|
||||||
Manage user accounts and permissions.
|
Manage user accounts and permissions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{!showAddUser ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowAddUser(true)}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
>
|
||||||
|
+ Add User
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="add-user-form" style={{
|
||||||
|
background: 'var(--background)',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
marginBottom: '1rem'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', fontSize: '1rem', fontWeight: 600 }}>Add New User</h3>
|
||||||
|
<div className="settings-form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={newUserEmail}
|
||||||
|
onChange={(e) => setNewUserEmail(e.target.value)}
|
||||||
|
placeholder="user@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="settings-form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newUserPassword}
|
||||||
|
onChange={(e) => setNewUserPassword(e.target.value)}
|
||||||
|
placeholder="Minimum 8 characters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="settings-form-group">
|
||||||
|
<label>Role</label>
|
||||||
|
<select
|
||||||
|
value={newUserRole}
|
||||||
|
onChange={(e) => setNewUserRole(e.target.value as 'user' | 'admin')}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.625rem 0.75rem',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
background: 'var(--surface)',
|
||||||
|
color: 'var(--text)',
|
||||||
|
fontSize: '0.875rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="settings-form-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleCreateUser}
|
||||||
|
disabled={isCreatingUser}
|
||||||
|
>
|
||||||
|
{isCreatingUser ? 'Creating...' : 'Create User'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddUser(false);
|
||||||
|
setNewUserEmail('');
|
||||||
|
setNewUserPassword('');
|
||||||
|
setNewUserRole('user');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoadingAdmin ? (
|
{isLoadingAdmin ? (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
|
||||||
<span className="spinner" />
|
<span className="spinner" />
|
||||||
|
|
@ -893,30 +1004,35 @@ export default function Settings() {
|
||||||
<td className="user-email">{user.email}</td>
|
<td className="user-email">{user.email}</td>
|
||||||
<td>{user.name || '-'}</td>
|
<td>{user.name || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
{user.is_admin && <span className="user-badge admin">Admin</span>}
|
{user.id === profile?.id ? (
|
||||||
|
<span className="user-badge admin">Admin (You)</span>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
value={user.is_admin ? 'admin' : 'user'}
|
||||||
|
onChange={(e) => handleRoleChange(user.id, e.target.value as 'user' | 'admin')}
|
||||||
|
style={{
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
background: 'var(--surface)',
|
||||||
|
color: 'var(--text)',
|
||||||
|
fontSize: '0.75rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{new Date(user.created_at).toLocaleDateString()}</td>
|
<td>{new Date(user.created_at).toLocaleDateString()}</td>
|
||||||
<td className="actions">
|
<td className="actions">
|
||||||
{user.id !== profile?.id && (
|
{user.id !== profile?.id && (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn btn-danger btn-sm"
|
||||||
className="btn btn-secondary btn-sm"
|
onClick={() => handleDeleteUser(user.id)}
|
||||||
onClick={() => handleToggleAdmin(user.id, user.is_admin)}
|
>
|
||||||
>
|
Delete
|
||||||
{user.is_admin ? 'Remove Admin' : 'Make Admin'}
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-danger btn-sm"
|
|
||||||
onClick={() => handleDeleteUser(user.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{user.id === profile?.id && (
|
|
||||||
<span style={{ color: 'var(--text-muted)', fontSize: '0.75rem' }}>
|
|
||||||
(You)
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue