Added admin panel and modified user action dropdown
This commit is contained in:
parent
78ab9acb04
commit
31f1fbae94
@ -7,6 +7,7 @@ import ArticleDetail from './components/ArticleDetail';
|
|||||||
import ArticleEditor from './components/ArticleEdit';
|
import ArticleEditor from './components/ArticleEdit';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Registration from './components/Registration'
|
import Registration from './components/Registration'
|
||||||
|
import AdminPanel from './components/AdminPanel';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
@ -202,9 +203,21 @@ function App() {
|
|||||||
setAuthView('register');
|
setAuthView('register');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenAdminPanel = () => {
|
||||||
|
setCurrentView('admin');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackFromAdmin = () => {
|
||||||
|
setCurrentView('list');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Header />
|
<Header
|
||||||
|
currentUser={isLoggedIn ? currentUser : null}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
onOpenAdminPanel={handleOpenAdminPanel}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Show Login/Registration if not logged in */}
|
{/* Show Login/Registration if not logged in */}
|
||||||
{!isLoggedIn ? (
|
{!isLoggedIn ? (
|
||||||
@ -221,11 +234,6 @@ function App() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="user-info">
|
|
||||||
<span>Welcome, {currentUser?.display_name || currentUser?.username}({currentUser?.role})</span>
|
|
||||||
<button className="logout-button" onClick={handleLogout}>Logout</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentView === 'list' ? (
|
{currentView === 'list' ? (
|
||||||
<>
|
<>
|
||||||
<SearchBar onSearch={handleSearch} />
|
<SearchBar onSearch={handleSearch} />
|
||||||
@ -244,9 +252,11 @@ function App() {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : currentView === 'edit' ? (
|
||||||
<ArticleEditor article={selectedArticle} onSave={handleSave} onCancel={handleCancelEdit} />
|
<ArticleEditor article={selectedArticle} onSave={handleSave} onCancel={handleCancelEdit} />
|
||||||
)}
|
) : currentView === 'admin' ? (
|
||||||
|
<AdminPanel token={token} onBack={handleBackFromAdmin} />
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
158
client/kb-frontend/src/components/AdminPanel.css
Normal file
158
client/kb-frontend/src/components/AdminPanel.css
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
.admin-panel {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #242424;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-table th,
|
||||||
|
.users-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-table th {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-table tr:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-badge.local {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-badge.entra {
|
||||||
|
background-color: #0078d4;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.admin {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.editor {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.user {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background-color: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
191
client/kb-frontend/src/components/AdminPanel.jsx
Normal file
191
client/kb-frontend/src/components/AdminPanel.jsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './AdminPanel.css';
|
||||||
|
|
||||||
|
function AdminPanel({ token, onBack }) {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [editingUserId, setEditingUserId] = useState(null);
|
||||||
|
const [selectedRole, setSelectedRole] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:9000/api/admin/users', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setUsers(data);
|
||||||
|
} else {
|
||||||
|
setError('Failed to fetch users');
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Error fetching users', err);
|
||||||
|
setError('Failed to connect to server');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleChange = (userId, currentRole) => {
|
||||||
|
setEditingUserId(userId);
|
||||||
|
setSelectedRole(currentRole);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveRole = async (userId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:9000/api/admin/users/${userId}/role`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ role: selectedRole })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setUsers(users.map(user => (
|
||||||
|
user.id === userId ? { ...user, role: selectedRole} : user
|
||||||
|
)));
|
||||||
|
setEditingUserId(null);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.error || 'Failed to update role');
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Error updating role', err);
|
||||||
|
alert('Failed to update role');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setEditingUserId(null);
|
||||||
|
setSelectedRole('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUser = async (userId) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this user? This cannot be undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:9000/api/admin/users/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setUsers(users.filter(user => user.id !== userId));
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.error || 'Failed to delete user');
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Failed to delete user', err);
|
||||||
|
alert('Failed to delete user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className='admin-panel'>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='admin-panel'>
|
||||||
|
<div className='admin-header'>
|
||||||
|
<button className='back-button' onClick={onBack}>← Back to Articles</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className='error-message'>{error}</div>}
|
||||||
|
|
||||||
|
<div className='admin-section'>
|
||||||
|
<h2>User Management</h2>
|
||||||
|
<table className='users-table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Display Name</th>
|
||||||
|
<th>Auth Provider</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map(user => (
|
||||||
|
<tr key ={user.id}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
<td>{user.display_name}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`auth-badge ${user.auth_provider}`}>
|
||||||
|
{user.auth_provider}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{editingUserId === user.id ? (
|
||||||
|
<select
|
||||||
|
value={selectedRole}
|
||||||
|
onChange={(e) => setSelectedRole(e.target.value)}
|
||||||
|
className='role-select'
|
||||||
|
>
|
||||||
|
<option value='Admin'>Admin</option>
|
||||||
|
<option value='Editor'>Editor</option>
|
||||||
|
<option value='User'>User</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span className={`role-badge ${user.role.toLowerCase()}`}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{new Date(user.created_at).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
{editingUserId === user.id ? (
|
||||||
|
<div className='action-buttons'>
|
||||||
|
<button
|
||||||
|
className='save-btn'
|
||||||
|
onClick={() => handleSaveRole(user.id)}
|
||||||
|
>Save</button>
|
||||||
|
<button
|
||||||
|
className='cancel-btn'
|
||||||
|
onClick={() => handleCancelEdit()}
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='action-buttons'>
|
||||||
|
<button
|
||||||
|
className='edit-btn'
|
||||||
|
onClick={() => handleRoleChange(user.id, user.role)}
|
||||||
|
>Edit Role</button>
|
||||||
|
<button
|
||||||
|
className='delete-btn'
|
||||||
|
onClick={() => handleDeleteUser(user.id)}
|
||||||
|
>Delete</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className='admin-section'>
|
||||||
|
<h2>Audit Logs</h2>
|
||||||
|
<p className='coming-soon'>Coming Soon...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPanel;
|
||||||
@ -2,7 +2,14 @@
|
|||||||
background-color: #2c3e50;
|
background-color: #2c3e50;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
import UserMenu from './UserMenu';
|
||||||
|
|
||||||
function Header() {
|
function Header({ currentUser, onLogout, onOpenAdminPanel }) {
|
||||||
return (
|
return (
|
||||||
<header className='header'>
|
<header className='header'>
|
||||||
|
<div className='header-content'>
|
||||||
<h1>Cram-A-Lot Knowledge Base</h1>
|
<h1>Cram-A-Lot Knowledge Base</h1>
|
||||||
|
{currentUser && (
|
||||||
|
<UserMenu
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={onLogout}
|
||||||
|
onOpenAdminPanel={onOpenAdminPanel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
client/kb-frontend/src/components/UserMenu.css
Normal file
80
client/kb-frontend/src/components/UserMenu.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
.user-menu {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background-color: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #34495e;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-button:hover {
|
||||||
|
background-color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
color: #bdc3c7;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.admin-item {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.admin-item:hover {
|
||||||
|
background-color: #ffe5e5;
|
||||||
|
}
|
||||||
54
client/kb-frontend/src/components/UserMenu.jsx
Normal file
54
client/kb-frontend/src/components/UserMenu.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import './UserMenu.css';
|
||||||
|
|
||||||
|
function UserMenu({ currentUser, onLogout, onOpenAdminPanel }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClick = (action) => {
|
||||||
|
setIsOpen(false);
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='user-menu' ref={menuRef}>
|
||||||
|
<button className='user-menu-button' onClick={toggleMenu}>
|
||||||
|
<span className='user-name'>{currentUser?.display_name}</span>
|
||||||
|
<span className='user-role'>{currentUser?.role}</span>
|
||||||
|
<span className={`dropdown-arrow ${isOpen ? 'open' : ''}`}>▼</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className='user-menu-dropdown'>
|
||||||
|
{currentUser?.role === 'Admin' && (
|
||||||
|
<button
|
||||||
|
className='menu-item admin-item'
|
||||||
|
onClick={() => handleMenuClick(onOpenAdminPanel)}
|
||||||
|
>Admin Panel</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className='menu-item'
|
||||||
|
onClick={() => handleMenuClick(onLogout)}
|
||||||
|
>Logout</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserMenu;
|
||||||
@ -64,7 +64,8 @@ function authenticateToken(req, res, next) {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name,
|
||||||
auth_provider: user.auth_provider
|
auth_provider: user.auth_provider,
|
||||||
|
role: user.role
|
||||||
};
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|||||||
57
server/db.js
57
server/db.js
@ -285,6 +285,58 @@ function getUserById(id) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the saved user's role
|
||||||
|
* @param {Int} userId - The Id for the user
|
||||||
|
* @param {string} newRole - The new role to be assigned
|
||||||
|
* @returns {Object} - The updated user object
|
||||||
|
*/
|
||||||
|
function updateUserRole(userId, newRole) {
|
||||||
|
db.run("UPDATE users SET role = ? WHERE id = ?",
|
||||||
|
[newRole, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = db.export();
|
||||||
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
||||||
|
|
||||||
|
return getUserById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the user from the database
|
||||||
|
* @param {Int} userId - The Id of the user to be deleted
|
||||||
|
*/
|
||||||
|
function deleteUser(userId) {
|
||||||
|
db.run("DELETE FROM users WHERE id = ?", [userId]);
|
||||||
|
|
||||||
|
const data = db.export();
|
||||||
|
fs.writeFileSync(DB_PATH. Buffer.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all users currently stored in the database
|
||||||
|
* @returns {[Object]} - an array of user objects
|
||||||
|
*/
|
||||||
|
function getAllUsers() {
|
||||||
|
const result = db.exec(
|
||||||
|
"SELECT id, username, email, display_name, auth_provider, role, created_at FROM users ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length === 0) return [];
|
||||||
|
|
||||||
|
const columns = result[0].columns;
|
||||||
|
const rows = result[0].values;
|
||||||
|
|
||||||
|
const users = rows.map(row => {
|
||||||
|
const user = {};
|
||||||
|
columns.forEach((col, index) => {
|
||||||
|
user[col] = row[index];
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initDb,
|
initDb,
|
||||||
getAllArticles,
|
getAllArticles,
|
||||||
@ -296,5 +348,8 @@ module.exports = {
|
|||||||
createUser,
|
createUser,
|
||||||
getUserByUsername,
|
getUserByUsername,
|
||||||
getUserByEmail,
|
getUserByEmail,
|
||||||
getUserById
|
getUserById,
|
||||||
|
updateUserRole,
|
||||||
|
deleteUser,
|
||||||
|
getAllUsers
|
||||||
};
|
};
|
||||||
@ -10,7 +10,11 @@ const {
|
|||||||
searchArticles,
|
searchArticles,
|
||||||
createUser,
|
createUser,
|
||||||
getUserByUsername,
|
getUserByUsername,
|
||||||
getUserByEmail
|
getUserByEmail,
|
||||||
|
getUserById,
|
||||||
|
getAllUsers,
|
||||||
|
updateUserRole,
|
||||||
|
deleteUser
|
||||||
} = require('./db');
|
} = require('./db');
|
||||||
const { generateToken, authenticateToken, authorizeRoles } = require('./auth');
|
const { generateToken, authenticateToken, authorizeRoles } = require('./auth');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -272,6 +276,61 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/admin/users', authenticateToken, authorizeRoles('Admin'), (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = getAllUsers();
|
||||||
|
res.status(200).json(users);
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
res.status(500).json({error: 'Failed to fetch users', details: String(error)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/admin/users/:id/role', authenticateToken, authorizeRoles('Admin'), (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = parseInt(req.params.id);
|
||||||
|
const { role } = req.body;
|
||||||
|
|
||||||
|
if (!['Admin', 'Editor', 'User'].includes(role)) {
|
||||||
|
return res.status(400).json({error: 'Invalid role. Must be Admin, Editor, or User'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId === req.user.id) {
|
||||||
|
return res.status(400).json({error: 'You cannot change your own role'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({error: 'User not found'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = updateUserRole(userId, role);
|
||||||
|
res.status(200).json(updatedUser);
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error updating user:', error);
|
||||||
|
return res.status(500).json({error: 'Failed to update user role', details: String(error)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/admin/users/:id', authenticateToken, authorizeRoles('Admin'), (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
if (userId === req.user.id) {
|
||||||
|
return res.status(400).json({error: 'You cannot delete your own user profile'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserById(userId);
|
||||||
|
if (!user) return res.status(404).json({error: 'User not found'});
|
||||||
|
|
||||||
|
deleteUser(userId);
|
||||||
|
return res.status(200).json({message: 'User deleted successfully'});
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error deleting user:', error);
|
||||||
|
return res.status(500).json({error: 'Failed to delete user', details: String(error)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user