Added admin panel and modified user action dropdown

This commit is contained in:
MattLeo 2025-12-03 17:19:18 -06:00
parent 78ab9acb04
commit 31f1fbae94
10 changed files with 644 additions and 19 deletions

View File

@ -7,6 +7,7 @@ import ArticleDetail from './components/ArticleDetail';
import ArticleEditor from './components/ArticleEdit';
import Login from './components/Login';
import Registration from './components/Registration'
import AdminPanel from './components/AdminPanel';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
@ -202,9 +203,21 @@ function App() {
setAuthView('register');
};
const handleOpenAdminPanel = () => {
setCurrentView('admin');
};
const handleBackFromAdmin = () => {
setCurrentView('list');
};
return (
<div className="app">
<Header />
<Header
currentUser={isLoggedIn ? currentUser : null}
onLogout={handleLogout}
onOpenAdminPanel={handleOpenAdminPanel}
/>
{/* Show Login/Registration if not logged in */}
{!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' ? (
<>
<SearchBar onSearch={handleSearch} />
@ -244,9 +252,11 @@ function App() {
onDelete={handleDelete}
currentUser={currentUser}
/>
) : (
) : currentView === 'edit' ? (
<ArticleEditor article={selectedArticle} onSave={handleSave} onCancel={handleCancelEdit} />
)}
) : currentView === 'admin' ? (
<AdminPanel token={token} onBack={handleBackFromAdmin} />
) : null}
</>
)}
</div>

View 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;
}

View 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;

View File

@ -1,11 +1,18 @@
.header {
background-color: #2c3e50;
color: white;
padding: 1rem 2rem;
text-align: center;
background-color: #2c3e50;
color: white;
padding: 1rem 2rem;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1400px;
margin: 0 auto;
}
.header h1 {
margin: 0;
font-size: 2rem;
margin: 0;
font-size: 2rem;
}

View File

@ -1,9 +1,19 @@
import './Header.css';
import UserMenu from './UserMenu';
function Header() {
function Header({ currentUser, onLogout, onOpenAdminPanel }) {
return (
<header className='header'>
<h1>Cram-A-Lot Knowledge Base</h1>
<div className='header-content'>
<h1>Cram-A-Lot Knowledge Base</h1>
{currentUser && (
<UserMenu
currentUser={currentUser}
onLogout={onLogout}
onOpenAdminPanel={onOpenAdminPanel}
/>
)}
</div>
</header>
);
}

View 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;
}

View 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;

View File

@ -64,7 +64,8 @@ function authenticateToken(req, res, next) {
email: user.email,
username: user.username,
display_name: user.display_name,
auth_provider: user.auth_provider
auth_provider: user.auth_provider,
role: user.role
};
next();

View File

@ -285,6 +285,58 @@ function getUserById(id) {
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 = {
initDb,
getAllArticles,
@ -296,5 +348,8 @@ module.exports = {
createUser,
getUserByUsername,
getUserByEmail,
getUserById
getUserById,
updateUserRole,
deleteUser,
getAllUsers
};

View File

@ -10,7 +10,11 @@ const {
searchArticles,
createUser,
getUserByUsername,
getUserByEmail
getUserByEmail,
getUserById,
getAllUsers,
updateUserRole,
deleteUser
} = require('./db');
const { generateToken, authenticateToken, authorizeRoles } = require('./auth');
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, () => {
console.log(`Server running on http://localhost:${PORT}`);
});