feat: add app-level authentication with login, session cookies, and admin panel

Replace Nginx basic auth with proper user accounts:
- Session cookies via itsdangerous (30-day expiry, httponly, secure)
- Password hashing with bcrypt via passlib
- Login page with AC-themed UI
- Admin page for user management (CRUD)
- AuthMiddleware exempts plugin WS and browser WS endpoints
- Issues/comments author auto-populated from session
- Sidebar shows logged-in username, admin link, and logout
- Seed users: erik (admin), alex, lundberg
- SECRET_KEY env var for cookie signing
This commit is contained in:
Erik 2026-04-10 19:45:08 +02:00
parent fac5063878
commit b09169ade2
9 changed files with 878 additions and 60 deletions

View file

@ -4125,6 +4125,26 @@ fetch('/api-version').then(r => r.json()).then(d => {
if (el) el.textContent = 'v' + d.version;
}).catch(() => {});
// ─── Current User Info ──────────────────────────────────────────
let _currentUser = null;
fetch('/me').then(r => {
if (!r.ok) throw new Error('not authenticated');
return r.json();
}).then(data => {
_currentUser = data;
const userInfo = document.getElementById('userInfo');
const nameEl = document.getElementById('currentUsername');
const adminLink = document.getElementById('adminLink');
if (userInfo && nameEl) {
nameEl.textContent = data.username;
userInfo.style.display = 'flex';
}
if (adminLink && data.is_admin) {
adminLink.style.display = 'inline';
}
}).catch(() => {});
// ─── Issues Board ───────────────────────────────────────────────
const ISSUE_CATEGORIES = {
@ -4165,7 +4185,6 @@ function showIssuesWindow() {
form.className = 'issues-form';
form.innerHTML = `
<div style="display:flex;gap:4px;margin-bottom:4px;">
<input type="text" id="issueAuthor" placeholder="Your name..." style="width:120px;padding:3px 6px;font-size:0.8rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
<input type="text" id="issueTitle" placeholder="Issue title..." style="flex:1;padding:3px 6px;font-size:0.8rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
<select id="issueCategory" style="padding:3px;font-size:0.75rem;border:1px solid #555;background:#2a2a2a;color:#ddd;">
<option value="plugin">Plugin</option>
@ -4182,24 +4201,17 @@ function showIssuesWindow() {
`;
content.appendChild(form);
// Remember author name in localStorage
const authorInput = form.querySelector('#issueAuthor');
authorInput.value = localStorage.getItem('issueAuthorName') || '';
// Add button handler
form.querySelector('#issueAddBtn').addEventListener('click', async () => {
const author = document.getElementById('issueAuthor').value.trim() || 'Anonymous';
const title = document.getElementById('issueTitle').value.trim();
const desc = document.getElementById('issueDescription').value.trim();
const cat = document.getElementById('issueCategory').value;
if (!title) return;
localStorage.setItem('issueAuthorName', author);
await fetch('/issues', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description: desc, category: cat, author })
body: JSON.stringify({ title, description: desc, category: cat })
});
document.getElementById('issueTitle').value = '';
document.getElementById('issueDescription').value = '';
@ -4406,11 +4418,10 @@ function showCommentsSection(row, issue, win) {
addDiv.querySelector('.comment-add-btn').addEventListener('click', async () => {
const text = addDiv.querySelector('.comment-text-input').value.trim();
if (!text) return;
const author = localStorage.getItem('issueAuthorName') || 'Anonymous';
await fetch(`/issues/${issue.id}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, author })
body: JSON.stringify({ text })
});
refreshIssuesList(win);
});