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() {
) : (
<>
-
-
+ {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';