diff --git a/client/kb-frontend/src/App.jsx b/client/kb-frontend/src/App.jsx
index 15419e1..6489c29 100644
--- a/client/kb-frontend/src/App.jsx
+++ b/client/kb-frontend/src/App.jsx
@@ -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 (
-
+
{/* Show Login/Registration if not logged in */}
{!isLoggedIn ? (
@@ -221,11 +234,6 @@ function App() {
)
) : (
<>
-
- Welcome, {currentUser?.display_name || currentUser?.username}({currentUser?.role})
-
-
-
{currentView === 'list' ? (
<>
@@ -244,9 +252,11 @@ function App() {
onDelete={handleDelete}
currentUser={currentUser}
/>
- ) : (
+ ) : currentView === 'edit' ? (
- )}
+ ) : currentView === 'admin' ? (
+
+ ) : null}
>
)}
diff --git a/client/kb-frontend/src/components/AdminPanel.css b/client/kb-frontend/src/components/AdminPanel.css
new file mode 100644
index 0000000..5721226
--- /dev/null
+++ b/client/kb-frontend/src/components/AdminPanel.css
@@ -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;
+}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/AdminPanel.jsx b/client/kb-frontend/src/components/AdminPanel.jsx
new file mode 100644
index 0000000..f71d849
--- /dev/null
+++ b/client/kb-frontend/src/components/AdminPanel.jsx
@@ -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 Loading...
+ }
+
+ return (
+
+
+
+
+
+ {error &&
{error}
}
+
+
+
User Management
+
+
+
+ | Username |
+ Email |
+ Display Name |
+ Auth Provider |
+ Role |
+ Created |
+ Actions |
+
+
+
+ {users.map(user => (
+
+ | {user.username} |
+ {user.email} |
+ {user.display_name} |
+
+
+ {user.auth_provider}
+
+ |
+
+ {editingUserId === user.id ? (
+
+ ) : (
+
+ {user.role}
+
+ )}
+ |
+ {new Date(user.created_at).toLocaleDateString()} |
+
+ {editingUserId === user.id ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+ |
+
+ ))}
+
+
+
+
+
Audit Logs
+
Coming Soon...
+
+
+ );
+}
+
+export default AdminPanel;
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/Header.css b/client/kb-frontend/src/components/Header.css
index ac88d53..b2d02ee 100644
--- a/client/kb-frontend/src/components/Header.css
+++ b/client/kb-frontend/src/components/Header.css
@@ -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;
}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/Header.jsx b/client/kb-frontend/src/components/Header.jsx
index af8408e..baab426 100644
--- a/client/kb-frontend/src/components/Header.jsx
+++ b/client/kb-frontend/src/components/Header.jsx
@@ -1,9 +1,19 @@
import './Header.css';
+import UserMenu from './UserMenu';
-function Header() {
+function Header({ currentUser, onLogout, onOpenAdminPanel }) {
return (
);
}
diff --git a/client/kb-frontend/src/components/UserMenu.css b/client/kb-frontend/src/components/UserMenu.css
new file mode 100644
index 0000000..62d2037
--- /dev/null
+++ b/client/kb-frontend/src/components/UserMenu.css
@@ -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;
+}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/UserMenu.jsx b/client/kb-frontend/src/components/UserMenu.jsx
new file mode 100644
index 0000000..1655d06
--- /dev/null
+++ b/client/kb-frontend/src/components/UserMenu.jsx
@@ -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 (
+
+
+
+ {isOpen && (
+
+ {currentUser?.role === 'Admin' && (
+
+ )}
+
+
+ )}
+
+ );
+}
+
+export default UserMenu;
diff --git a/server/auth.js b/server/auth.js
index aea9151..0d9abd7 100644
--- a/server/auth.js
+++ b/server/auth.js
@@ -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();
diff --git a/server/db.js b/server/db.js
index 1f9059e..c9b0ffb 100644
--- a/server/db.js
+++ b/server/db.js
@@ -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
};
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index 1f87f0c..4ba9098 100644
--- a/server/server.js
+++ b/server/server.js
@@ -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}`);
});