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

+ + + + + + + + + + + + + + {users.map(user => ( + + + + + + + + + + ))} + +
UsernameEmailDisplay NameAuth ProviderRoleCreatedActions
{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 (
-

Cram-A-Lot Knowledge Base

+
+

Cram-A-Lot Knowledge Base

+ {currentUser && ( + + )} +
); } 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}`); });