diff --git a/client/kb-frontend/src/App.jsx b/client/kb-frontend/src/App.jsx index 499be6a..15419e1 100644 --- a/client/kb-frontend/src/App.jsx +++ b/client/kb-frontend/src/App.jsx @@ -57,6 +57,7 @@ function App() { localStorage.removeItem('token'); localStorage.removeItem('user'); + localStorage.setItem('manualLogout', 'true'); }; const fetchArticles = () => { @@ -221,20 +222,28 @@ function App() { ) : ( <>
- Welcome, {currentUser?.display_name || currentUser?.username}! + Welcome, {currentUser?.display_name || currentUser?.username}({currentUser?.role})
{currentView === 'list' ? ( <> - + {(currentUser?.role === 'Admin' || currentUser?.role === 'Editor') && ( + + )} ) : currentView === 'detail' ? ( - + ) : ( )} diff --git a/client/kb-frontend/src/components/ArticleDetail.jsx b/client/kb-frontend/src/components/ArticleDetail.jsx index 3bc3493..3263b68 100644 --- a/client/kb-frontend/src/components/ArticleDetail.jsx +++ b/client/kb-frontend/src/components/ArticleDetail.jsx @@ -1,22 +1,28 @@ import './ArticleDetail.css'; -function ArticleDetail({ article, onBack, onEdit, onDelete }) { +function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) { if (!article) { return
No article selected
} + const canEdit = currentUser?.role === 'Admin' || currentUser?.role === 'Editor'; + return (
- - + {canEdit && ( + <> + + + + )}
diff --git a/client/kb-frontend/src/components/Login.jsx b/client/kb-frontend/src/components/Login.jsx index cfb0318..37e4fd2 100644 --- a/client/kb-frontend/src/components/Login.jsx +++ b/client/kb-frontend/src/components/Login.jsx @@ -9,6 +9,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) { const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [msalInstance, setMsalInstance] = useState(null); + const [autoLoginAttempted, setAutoLoginAttempted] = useState (false); useEffect(() => { const initializeMasal = async () => { @@ -20,6 +21,14 @@ function Login({ onLoginSuccess, onSwitchToRegister }) { initializeMasal(); }, []); + useEffect(() => { + const manualLogout = localStorage.getItem('manualLogout'); + if (msalInstance && !autoLoginAttempted && manualLogout !== 'true') { + setAutoLoginAttempted(true); + handleMicrosoftLogin(); + } + }, [msalInstance]) + const handleSubmit = async (e) => { e.preventDefault(); setError(''); @@ -40,6 +49,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) { const data = await response.json(); if (response.ok) { + localStorage.removeItem('manualLogout'); onLoginSuccess(data.user, data.token); } else { setError(data.error || 'Login failed'); @@ -72,13 +82,15 @@ function Login({ onLoginSuccess, onSwitchToRegister }) { 'Content-Type': 'application/json' }, body: JSON.stringify({ - accessToken: loginResponse.accessToken // Send access token instead of code + accessToken: loginResponse.accessToken, + idToken: loginResponse.idToken }) }); const data = await response.json(); if (response.ok) { + localStorage.removeItem('manualLogout'); onLoginSuccess(data.user, data.token); } else { setError(data.error || 'Microsoft login failed'); diff --git a/fd-client/.fdk/localstore b/fd-client/.fdk/localstore index 80326cc..0dfbb53 100644 --- a/fd-client/.fdk/localstore +++ b/fd-client/.fdk/localstore @@ -1 +1 @@ -{"fd_cli_101_101_migrate_oauth_status":true,"report_hash":"8ee96e64b8a3f4c5139f4c95c579919721a060b0421ea476a0870b7403a8245e"} \ No newline at end of file +{"fd_cli_101_101_migrate_oauth_status":true,"report_hash":"ac127cdfb4fd074868d43aef801f187105d36d777c033140b5e2dc170c0e87c8"} diff --git a/fd-client/.report.json b/fd-client/.report.json index e69f101..647c6c9 100644 --- a/fd-client/.report.json +++ b/fd-client/.report.json @@ -2,22 +2,17 @@ "lints": [ { "severity": 1, - "value": "app\\scripts\\app.js::6: 'client' declared and assigned in different scopes. Possible asynchronous race condition.", + "value": "app\\scripts\\app.js::2: Expected rejection to be handled.", "type": "custom-js-lint" }, { "severity": 1, - "value": "app\\scripts\\app.js::21: Expected rejection to be handled.", + "value": "app\\scripts\\app.js::72: 'isExpanded' declared and assigned in different scopes. Possible asynchronous race condition.", "type": "custom-js-lint" }, { "severity": 1, - "value": "app\\scripts\\app.js::91: 'isExpanded' declared and assigned in different scopes. Possible asynchronous race condition.", - "type": "custom-js-lint" - }, - { - "severity": 1, - "value": "app\\scripts\\app.js::106: Expected rejection to be handled.", + "value": "app\\scripts\\app.js::87: Expected rejection to be handled.", "type": "custom-js-lint" } ], @@ -31,218 +26,48 @@ "column": 0 }, "end": { - "line": 91, + "line": 10, "column": 3 } }, "1": { "start": { "line": 2, - "column": 2 + "column": 4 }, "end": { - "line": 90, - "column": 5 + "line": 9, + "column": 7 } }, "2": { "start": { "line": 3, - "column": 4 + "column": 8 }, "end": { "line": 3, - "column": 41 + "column": 38 } }, "3": { "start": { "line": 5, - "column": 4 + "column": 8 }, "end": { - "line": 5, - "column": 21 + "line": 7, + "column": 11 } }, "4": { "start": { - "line": 8, - "column": 25 + "line": 6, + "column": 12 }, "end": { - "line": 59, - "column": 13 - } - }, - "5": { - "start": { - "line": 62, - "column": 6 - }, - "end": { - "line": 62, - "column": 64 - } - }, - "6": { - "start": { - "line": 65, - "column": 21 - }, - "end": { - "line": 65, - "column": 57 - } - }, - "7": { - "start": { - "line": 66, - "column": 21 - }, - "end": { - "line": 66, - "column": 64 - } - }, - "8": { - "start": { - "line": 67, - "column": 22 - }, - "end": { - "line": 67, - "column": 66 - } - }, - "9": { - "start": { - "line": 68, - "column": 24 - }, - "end": { - "line": 68, - "column": 64 - } - }, - "10": { - "start": { - "line": 69, - "column": 23 - }, - "end": { - "line": 69, - "column": 28 - } - }, - "11": { - "start": { - "line": 71, - "column": 6 - }, - "end": { - "line": 83, - "column": 9 - } - }, - "12": { - "start": { - "line": 72, - "column": 8 - }, - "end": { - "line": 72, - "column": 33 - } - }, - "13": { - "start": { - "line": 74, - "column": 8 - }, - "end": { - "line": 82, - "column": 9 - } - }, - "14": { - "start": { - "line": 75, - "column": 10 - }, - "end": { - "line": 75, - "column": 39 - } - }, - "15": { - "start": { - "line": 76, - "column": 10 - }, - "end": { - "line": 76, - "column": 42 - } - }, - "16": { - "start": { - "line": 77, - "column": 10 - }, - "end": { - "line": 77, - "column": 38 - } - }, - "17": { - "start": { - "line": 79, - "column": 10 - }, - "end": { - "line": 79, - "column": 39 - } - }, - "18": { - "start": { - "line": 80, - "column": 10 - }, - "end": { - "line": 80, - "column": 41 - } - }, - "19": { - "start": { - "line": 81, - "column": 10 - }, - "end": { - "line": 81, - "column": 38 - } - }, - "20": { - "start": { - "line": 87, - "column": 4 - }, - "end": { - "line": 89, - "column": 7 - } - }, - "21": { - "start": { - "line": 88, - "column": 6 - }, - "end": { - "line": 88, - "column": 40 + "line": 6, + "column": 52 } } }, @@ -265,7 +90,7 @@ "column": 58 }, "end": { - "line": 91, + "line": 10, "column": 1 } }, @@ -276,201 +101,92 @@ "decl": { "start": { "line": 2, - "column": 25 + "column": 27 }, "end": { "line": 2, - "column": 26 + "column": 28 } }, "loc": { "start": { "line": 2, - "column": 43 + "column": 45 }, "end": { - "line": 90, - "column": 3 + "line": 9, + "column": 5 } }, "line": 2 }, "2": { - "name": "injectKBDrawer", + "name": "(anonymous_2)", "decl": { "start": { - "line": 7, - "column": 13 + "line": 5, + "column": 75 + }, + "end": { + "line": 5, + "column": 76 + } + }, + "loc": { + "start": { + "line": 5, + "column": 86 }, "end": { "line": 7, - "column": 27 - } - }, - "loc": { - "start": { - "line": 7, - "column": 30 - }, - "end": { - "line": 84, - "column": 5 - } - }, - "line": 7 - }, - "3": { - "name": "(anonymous_3)", - "decl": { - "start": { - "line": 71, - "column": 39 - }, - "end": { - "line": 71, - "column": 40 - } - }, - "loc": { - "start": { - "line": 71, - "column": 51 - }, - "end": { - "line": 83, - "column": 7 - } - }, - "line": 71 - }, - "4": { - "name": "(anonymous_4)", - "decl": { - "start": { - "line": 87, - "column": 35 - }, - "end": { - "line": 87, - "column": 36 - } - }, - "loc": { - "start": { - "line": 87, - "column": 51 - }, - "end": { - "line": 89, - "column": 5 - } - }, - "line": 87 - } - }, - "branchMap": { - "0": { - "loc": { - "start": { - "line": 74, - "column": 8 - }, - "end": { - "line": 82, "column": 9 } }, - "type": "if", - "locations": [ - { - "start": { - "line": 74, - "column": 8 - }, - "end": { - "line": 82, - "column": 9 - } - }, - { - "start": { - "line": 78, - "column": 15 - }, - "end": { - "line": 82, - "column": 9 - } - } - ], - "line": 74 + "line": 5 } }, + "branchMap": {}, "s": { - "0": 8, - "1": 8, - "2": 7, - "3": 7, - "4": 7, - "5": 7, - "6": 7, - "7": 7, - "8": 7, - "9": 7, - "10": 7, - "11": 7, - "12": 0, - "13": 0, - "14": 0, - "15": 0, - "16": 0, - "17": 0, - "18": 0, - "19": 0, - "20": 7, - "21": 7 + "0": 12, + "1": 12, + "2": 6, + "3": 6, + "4": 1 }, "f": { - "0": 8, - "1": 7, - "2": 7, - "3": 0, - "4": 7 - }, - "b": { - "0": [ - 0, - 0 - ] + "0": 12, + "1": 6, + "2": 1 }, + "b": {}, "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", - "hash": "62a764b53a2397355729862d774423d12cc8d951" + "hash": "18566891b473df9f163282f1310c6fc8160a5fe6" } }, "coverageSummary": { "lines": { - "total": 22, - "covered": 14, + "total": 5, + "covered": 5, "skipped": 0, - "pct": 63.63 + "pct": 100 }, "statements": { - "total": 22, - "covered": 14, + "total": 5, + "covered": 5, "skipped": 0, - "pct": 63.63 + "pct": 100 }, "functions": { - "total": 5, - "covered": 4, + "total": 3, + "covered": 3, "skipped": 0, - "pct": 80 + "pct": 100 }, "branches": { - "total": 2, + "total": 0, "covered": 0, "skipped": 0, - "pct": 0 + "pct": 100 } } } diff --git a/server/auth.js b/server/auth.js index be09535..aea9151 100644 --- a/server/auth.js +++ b/server/auth.js @@ -73,9 +73,32 @@ function authenticateToken(req, res, next) { } } + +/** + * Middleware to check if user has required role + * @param {Array} allowedRoles - Array of roles that can acces the route + */ +function authorizeRoles(...allowedRoles) { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({error: 'Authentication requried'}); + } + + if (!allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + error: 'Insufficient permissions', + required: allowedRoles, + current: req.user.role + }); + } + next(); + }; +} + module.exports = { generateToken, verifyToken, authenticateToken, + authorizeRoles, JWT_SECRET }; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 0e93633..1f87f0c 100644 --- a/server/server.js +++ b/server/server.js @@ -12,11 +12,12 @@ const { getUserByUsername, getUserByEmail } = require('./db'); -const { generateToken, authenticateToken } = require('./auth'); +const { generateToken, authenticateToken, authorizeRoles } = require('./auth'); const app = express(); const cors = require('cors'); const msal = require('@azure/msal-node'); const entraConfig = require('./entraConfig'); +const jwt = require('jsonwebtoken'); const PORT = 9000; app.use(cors()) @@ -55,7 +56,7 @@ initDb().then(() => { } }); - app.post('/api/articles', authenticateToken, (req, res) => { + app.post('/api/articles', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { title, content } = req.body; @@ -72,7 +73,7 @@ initDb().then(() => { } }); - app.put('/api/articles/:ka_number', authenticateToken, (req, res) => { + app.put('/api/articles/:ka_number', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { title, content } = req.body; @@ -93,7 +94,7 @@ initDb().then(() => { } }); - app.delete('/api/articles/:ka_number', authenticateToken, (req, res) => { + app.delete('/api/articles/:ka_number', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { deleteArticle(req.params.ka_number); return res.status(200).json({'message': 'Successfully deleted article'}); @@ -149,7 +150,7 @@ initDb().then(() => { email: newUser.email, display_name: newUser.display_name, auth_provider: newUser.auth_provider, - role: user.role, + role: newUser.role, created_at: newUser.created_at }, token @@ -202,7 +203,7 @@ initDb().then(() => { app.post('/api/auth/microsoft', async (req, res) => { try { - const { accessToken } = req.body; + const { accessToken, idToken } = req.body; if (!accessToken) { return res.status(400).json({error: 'Access token required'}); @@ -219,8 +220,8 @@ initDb().then(() => { return res.status(401).json({error: 'Invalid Microsoft token'}); } - const decoded = jwt.decode(accessToken); - const roles = decoded.roles || {}; + const decoded = jwt.decode(idToken); + const roles = decoded.roles || []; let userRole = 'User'; if (roles.includes('Admin')) userRole = 'Admin';