added role permissions on actions and autologin

This commit is contained in:
MattLeo 2025-12-03 13:03:20 -06:00
parent f103f0cb4f
commit 78ab9acb04
7 changed files with 131 additions and 364 deletions

View File

@ -57,6 +57,7 @@ function App() {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.setItem('manualLogout', 'true');
}; };
const fetchArticles = () => { const fetchArticles = () => {
@ -221,20 +222,28 @@ function App() {
) : ( ) : (
<> <>
<div className="user-info"> <div className="user-info">
<span>Welcome, {currentUser?.display_name || currentUser?.username}!</span> <span>Welcome, {currentUser?.display_name || currentUser?.username}({currentUser?.role})</span>
<button className="logout-button" onClick={handleLogout}>Logout</button> <button className="logout-button" onClick={handleLogout}>Logout</button>
</div> </div>
{currentView === 'list' ? ( {currentView === 'list' ? (
<> <>
<SearchBar onSearch={handleSearch} /> <SearchBar onSearch={handleSearch} />
{(currentUser?.role === 'Admin' || currentUser?.role === 'Editor') && (
<button className='create-new-button' onClick={handleCreateNew}> <button className='create-new-button' onClick={handleCreateNew}>
+ Create New Article + Create New Article
</button> </button>
)}
<ArticleList articles={articles} onArticleClick={handleArticleClick} /> <ArticleList articles={articles} onArticleClick={handleArticleClick} />
</> </>
) : currentView === 'detail' ? ( ) : currentView === 'detail' ? (
<ArticleDetail article={selectedArticle} onBack={handleBack} onEdit={handleEdit} onDelete={handleDelete} /> <ArticleDetail
article={selectedArticle}
onBack={handleBack}
onEdit={handleEdit}
onDelete={handleDelete}
currentUser={currentUser}
/>
) : ( ) : (
<ArticleEditor article={selectedArticle} onSave={handleSave} onCancel={handleCancelEdit} /> <ArticleEditor article={selectedArticle} onSave={handleSave} onCancel={handleCancelEdit} />
)} )}

View File

@ -1,22 +1,28 @@
import './ArticleDetail.css'; import './ArticleDetail.css';
function ArticleDetail({ article, onBack, onEdit, onDelete }) { function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) {
if (!article) { if (!article) {
return <div>No article selected</div> return <div>No article selected</div>
} }
const canEdit = currentUser?.role === 'Admin' || currentUser?.role === 'Editor';
return ( return (
<div className='article-detail'> <div className='article-detail'>
<div className='button-group'> <div className='button-group'>
<button className='back-button' onClick={onBack}> <button className='back-button' onClick={onBack}>
Back Back
</button> </button>
{canEdit && (
<>
<button className='edit-button' onClick={onEdit}> <button className='edit-button' onClick={onEdit}>
Edit Edit
</button> </button>
<button className='delete-button' onClick={onDelete}> <button className='delete-button' onClick={onDelete}>
Delete Delete
</button> </button>
</>
)}
</div> </div>
<div className='article-header'> <div className='article-header'>

View File

@ -9,6 +9,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [msalInstance, setMsalInstance] = useState(null); const [msalInstance, setMsalInstance] = useState(null);
const [autoLoginAttempted, setAutoLoginAttempted] = useState (false);
useEffect(() => { useEffect(() => {
const initializeMasal = async () => { const initializeMasal = async () => {
@ -20,6 +21,14 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
initializeMasal(); initializeMasal();
}, []); }, []);
useEffect(() => {
const manualLogout = localStorage.getItem('manualLogout');
if (msalInstance && !autoLoginAttempted && manualLogout !== 'true') {
setAutoLoginAttempted(true);
handleMicrosoftLogin();
}
}, [msalInstance])
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
@ -40,6 +49,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
localStorage.removeItem('manualLogout');
onLoginSuccess(data.user, data.token); onLoginSuccess(data.user, data.token);
} else { } else {
setError(data.error || 'Login failed'); setError(data.error || 'Login failed');
@ -72,13 +82,15 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
accessToken: loginResponse.accessToken // Send access token instead of code accessToken: loginResponse.accessToken,
idToken: loginResponse.idToken
}) })
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
localStorage.removeItem('manualLogout');
onLoginSuccess(data.user, data.token); onLoginSuccess(data.user, data.token);
} else { } else {
setError(data.error || 'Microsoft login failed'); setError(data.error || 'Microsoft login failed');

View File

@ -1 +1 @@
{"fd_cli_101_101_migrate_oauth_status":true,"report_hash":"8ee96e64b8a3f4c5139f4c95c579919721a060b0421ea476a0870b7403a8245e"} {"fd_cli_101_101_migrate_oauth_status":true,"report_hash":"ac127cdfb4fd074868d43aef801f187105d36d777c033140b5e2dc170c0e87c8"}

View File

@ -2,22 +2,17 @@
"lints": [ "lints": [
{ {
"severity": 1, "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" "type": "custom-js-lint"
}, },
{ {
"severity": 1, "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" "type": "custom-js-lint"
}, },
{ {
"severity": 1, "severity": 1,
"value": "app\\scripts\\app.js::91: 'isExpanded' declared and assigned in different scopes. Possible asynchronous race condition.", "value": "app\\scripts\\app.js::87: Expected rejection to be handled.",
"type": "custom-js-lint"
},
{
"severity": 1,
"value": "app\\scripts\\app.js::106: Expected rejection to be handled.",
"type": "custom-js-lint" "type": "custom-js-lint"
} }
], ],
@ -31,218 +26,48 @@
"column": 0 "column": 0
}, },
"end": { "end": {
"line": 91, "line": 10,
"column": 3 "column": 3
} }
}, },
"1": { "1": {
"start": { "start": {
"line": 2, "line": 2,
"column": 2 "column": 4
}, },
"end": { "end": {
"line": 90, "line": 9,
"column": 5 "column": 7
} }
}, },
"2": { "2": {
"start": { "start": {
"line": 3, "line": 3,
"column": 4 "column": 8
}, },
"end": { "end": {
"line": 3, "line": 3,
"column": 41 "column": 38
} }
}, },
"3": { "3": {
"start": { "start": {
"line": 5, "line": 5,
"column": 4 "column": 8
}, },
"end": { "end": {
"line": 5, "line": 7,
"column": 21 "column": 11
} }
}, },
"4": { "4": {
"start": { "start": {
"line": 8, "line": 6,
"column": 25 "column": 12
}, },
"end": { "end": {
"line": 59, "line": 6,
"column": 13 "column": 52
}
},
"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
} }
} }
}, },
@ -265,7 +90,7 @@
"column": 58 "column": 58
}, },
"end": { "end": {
"line": 91, "line": 10,
"column": 1 "column": 1
} }
}, },
@ -276,201 +101,92 @@
"decl": { "decl": {
"start": { "start": {
"line": 2, "line": 2,
"column": 25 "column": 27
}, },
"end": { "end": {
"line": 2, "line": 2,
"column": 26 "column": 28
} }
}, },
"loc": { "loc": {
"start": { "start": {
"line": 2, "line": 2,
"column": 43 "column": 45
}, },
"end": { "end": {
"line": 90, "line": 9,
"column": 3 "column": 5
} }
}, },
"line": 2 "line": 2
}, },
"2": { "2": {
"name": "injectKBDrawer", "name": "(anonymous_2)",
"decl": { "decl": {
"start": { "start": {
"line": 7, "line": 5,
"column": 13 "column": 75
},
"end": {
"line": 5,
"column": 76
}
},
"loc": {
"start": {
"line": 5,
"column": 86
}, },
"end": { "end": {
"line": 7, "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 "column": 9
} }
}, },
"type": "if", "line": 5
"locations": [
{
"start": {
"line": 74,
"column": 8
},
"end": {
"line": 82,
"column": 9
}
},
{
"start": {
"line": 78,
"column": 15
},
"end": {
"line": 82,
"column": 9
}
}
],
"line": 74
} }
}, },
"branchMap": {},
"s": { "s": {
"0": 8, "0": 12,
"1": 8, "1": 12,
"2": 7, "2": 6,
"3": 7, "3": 6,
"4": 7, "4": 1
"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
}, },
"f": { "f": {
"0": 8, "0": 12,
"1": 7, "1": 6,
"2": 7, "2": 1
"3": 0,
"4": 7
},
"b": {
"0": [
0,
0
]
}, },
"b": {},
"_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9",
"hash": "62a764b53a2397355729862d774423d12cc8d951" "hash": "18566891b473df9f163282f1310c6fc8160a5fe6"
} }
}, },
"coverageSummary": { "coverageSummary": {
"lines": { "lines": {
"total": 22, "total": 5,
"covered": 14, "covered": 5,
"skipped": 0, "skipped": 0,
"pct": 63.63 "pct": 100
}, },
"statements": { "statements": {
"total": 22, "total": 5,
"covered": 14, "covered": 5,
"skipped": 0, "skipped": 0,
"pct": 63.63 "pct": 100
}, },
"functions": { "functions": {
"total": 5, "total": 3,
"covered": 4, "covered": 3,
"skipped": 0, "skipped": 0,
"pct": 80 "pct": 100
}, },
"branches": { "branches": {
"total": 2, "total": 0,
"covered": 0, "covered": 0,
"skipped": 0, "skipped": 0,
"pct": 0 "pct": 100
} }
} }
} }

View File

@ -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 = { module.exports = {
generateToken, generateToken,
verifyToken, verifyToken,
authenticateToken, authenticateToken,
authorizeRoles,
JWT_SECRET JWT_SECRET
}; };

View File

@ -12,11 +12,12 @@ const {
getUserByUsername, getUserByUsername,
getUserByEmail getUserByEmail
} = require('./db'); } = require('./db');
const { generateToken, authenticateToken } = require('./auth'); const { generateToken, authenticateToken, authorizeRoles } = require('./auth');
const app = express(); const app = express();
const cors = require('cors'); const cors = require('cors');
const msal = require('@azure/msal-node'); const msal = require('@azure/msal-node');
const entraConfig = require('./entraConfig'); const entraConfig = require('./entraConfig');
const jwt = require('jsonwebtoken');
const PORT = 9000; const PORT = 9000;
app.use(cors()) 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 { try {
const { title, content } = req.body; 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 { try {
const { title, content } = req.body; 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 { try {
deleteArticle(req.params.ka_number); deleteArticle(req.params.ka_number);
return res.status(200).json({'message': 'Successfully deleted article'}); return res.status(200).json({'message': 'Successfully deleted article'});
@ -149,7 +150,7 @@ initDb().then(() => {
email: newUser.email, email: newUser.email,
display_name: newUser.display_name, display_name: newUser.display_name,
auth_provider: newUser.auth_provider, auth_provider: newUser.auth_provider,
role: user.role, role: newUser.role,
created_at: newUser.created_at created_at: newUser.created_at
}, },
token token
@ -202,7 +203,7 @@ initDb().then(() => {
app.post('/api/auth/microsoft', async (req, res) => { app.post('/api/auth/microsoft', async (req, res) => {
try { try {
const { accessToken } = req.body; const { accessToken, idToken } = req.body;
if (!accessToken) { if (!accessToken) {
return res.status(400).json({error: 'Access token required'}); return res.status(400).json({error: 'Access token required'});
@ -219,8 +220,8 @@ initDb().then(() => {
return res.status(401).json({error: 'Invalid Microsoft token'}); return res.status(401).json({error: 'Invalid Microsoft token'});
} }
const decoded = jwt.decode(accessToken); const decoded = jwt.decode(idToken);
const roles = decoded.roles || {}; const roles = decoded.roles || [];
let userRole = 'User'; let userRole = 'User';
if (roles.includes('Admin')) userRole = 'Admin'; if (roles.includes('Admin')) userRole = 'Admin';