Skip to content

Commit 3b60a5f

Browse files
committed
login
1 parent a2651e5 commit 3b60a5f

5 files changed

Lines changed: 405 additions & 43 deletions

File tree

‎docker/api-handler.js‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const __dirname = path.dirname(__filename)
1414
export class APIHandler {
1515
constructor() {
1616
this.endpoint = Config.getSparqlEndpoint('newsmonitor')
17+
this.adminUsername = Config.getEnv('NEWSMONITOR_ADMIN_USERNAME', 'admin')
18+
this.adminPassword = Config.getEnv('NEWSMONITOR_ADMIN_PASSWORD')
1719
}
1820

1921
/**
@@ -362,8 +364,25 @@ export class APIHandler {
362364
const path = url.pathname
363365

364366
try {
367+
if (path === '/api/admin-check') {
368+
if (!this.requireAdminAuth(req, res)) {
369+
return true
370+
}
371+
372+
res.writeHead(200, {
373+
'Content-Type': 'application/json',
374+
'Access-Control-Allow-Origin': '*'
375+
})
376+
res.end(JSON.stringify({ status: 'ok', user: this.adminUsername }))
377+
return true
378+
}
379+
365380
// Subscribe to feeds (POST)
366381
if (path === '/api/subscribe' && req.method === 'POST') {
382+
if (!this.requireAdminAuth(req, res)) {
383+
return true
384+
}
385+
367386
const body = await this.parseBody(req)
368387
const urls = body.urls || []
369388

@@ -388,6 +407,10 @@ export class APIHandler {
388407

389408
// Unsubscribe from feed (POST)
390409
if (path === '/api/unsubscribe' && req.method === 'POST') {
410+
if (!this.requireAdminAuth(req, res)) {
411+
return true
412+
}
413+
391414
const body = await this.parseBody(req)
392415
const feedUri = body.feedUri
393416

@@ -409,6 +432,10 @@ export class APIHandler {
409432

410433
// Update all feeds (POST)
411434
if (path === '/api/update-feeds' && req.method === 'POST') {
435+
if (!this.requireAdminAuth(req, res)) {
436+
return true
437+
}
438+
412439
try {
413440
console.log('Manual feed update triggered via API')
414441
const result = await this.runTransCommand('src/apps/newsmonitor/update-all')
@@ -544,4 +571,53 @@ export class APIHandler {
544571
return true
545572
}
546573
}
574+
575+
requireAdminAuth(req, res) {
576+
if (!this.adminPassword) {
577+
console.error('Admin password missing: set NEWSMONITOR_ADMIN_PASSWORD in .env')
578+
res.writeHead(503, {
579+
'Content-Type': 'application/json',
580+
'Access-Control-Allow-Origin': '*'
581+
})
582+
res.end(JSON.stringify({ error: 'Admin password not configured' }))
583+
return false
584+
}
585+
586+
if (!this.isAdminAuthorized(req)) {
587+
res.writeHead(401, {
588+
'Content-Type': 'application/json',
589+
'Access-Control-Allow-Origin': '*',
590+
'WWW-Authenticate': 'Basic realm="NewsMonitor Admin"'
591+
})
592+
res.end(JSON.stringify({ error: 'Admin authentication required' }))
593+
return false
594+
}
595+
596+
return true
597+
}
598+
599+
isAdminAuthorized(req) {
600+
const authHeader = req.headers.authorization || ''
601+
if (!authHeader.startsWith('Basic ')) {
602+
return false
603+
}
604+
605+
const encoded = authHeader.slice('Basic '.length)
606+
let decoded
607+
try {
608+
decoded = Buffer.from(encoded, 'base64').toString('utf8')
609+
} catch (error) {
610+
return false
611+
}
612+
613+
const separatorIndex = decoded.indexOf(':')
614+
if (separatorIndex === -1) {
615+
return false
616+
}
617+
618+
const username = decoded.slice(0, separatorIndex)
619+
const password = decoded.slice(separatorIndex + 1)
620+
621+
return username === this.adminUsername && password === this.adminPassword
622+
}
547623
}

‎docker/public/admin.css‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
/* admin.css - Admin page specific styles */
1+
/* docker/public/admin.css */
2+
3+
/* Admin page specific styles */
24

35
.admin-nav {
46
margin-top: 20px;
@@ -9,6 +11,17 @@
911
flex-wrap: wrap;
1012
}
1113

14+
.admin-auth {
15+
display: flex;
16+
align-items: center;
17+
gap: 10px;
18+
}
19+
20+
.admin-status {
21+
font-size: 0.9rem;
22+
color: #6c757d;
23+
}
24+
1225
.admin-section {
1326
background: white;
1427
border-radius: 12px;
@@ -264,6 +277,34 @@
264277
color: #2c3e50;
265278
}
266279

280+
.modal-message {
281+
margin-bottom: 15px;
282+
color: #6c757d;
283+
}
284+
285+
.login-form {
286+
display: grid;
287+
gap: 10px;
288+
}
289+
290+
.login-form label {
291+
font-weight: 600;
292+
color: #2c3e50;
293+
font-size: 0.9rem;
294+
}
295+
296+
.login-form input {
297+
padding: 10px 12px;
298+
border: 2px solid #e0e0e0;
299+
border-radius: 8px;
300+
font-size: 0.95rem;
301+
}
302+
303+
.login-form input:focus {
304+
outline: none;
305+
border-color: #3498db;
306+
}
307+
267308
.modal-actions {
268309
margin-top: 25px;
269310
display: flex;

‎docker/public/admin.html‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- docker/public/admin.html -->
12
<!DOCTYPE html>
23
<html lang="en">
34
<head>
@@ -16,6 +17,10 @@ <h1>⚙️ NewsMonitor Admin</h1>
1617
<a href="/" class="btn-secondary">← Back to Feed</a>
1718
<a href="/about.html" class="btn-secondary">ℹ️ About</a>
1819
<button id="update-all-btn" class="btn-primary">🔄 Update All Feeds</button>
20+
<div class="admin-auth">
21+
<span id="admin-status" class="admin-status">Admin: logged out</span>
22+
<button id="admin-login-btn" class="btn-secondary">Admin Login</button>
23+
</div>
1924
</nav>
2025
<div id="update-status" class="status-message" style="display: none;"></div>
2126
</header>
@@ -96,6 +101,37 @@ <h3>Confirm Unsubscribe</h3>
96101
</div>
97102
</div>
98103

104+
<!-- Login Modal -->
105+
<div id="login-modal" class="modal" style="display: none;">
106+
<div class="modal-content">
107+
<h3>Admin Login</h3>
108+
<p id="login-message" class="modal-message">
109+
Log in to add or remove feeds.
110+
</p>
111+
<div class="login-form">
112+
<label for="login-username">Username</label>
113+
<input
114+
id="login-username"
115+
type="text"
116+
autocomplete="username"
117+
placeholder="admin"
118+
>
119+
<label for="login-password">Password</label>
120+
<input
121+
id="login-password"
122+
type="password"
123+
autocomplete="current-password"
124+
placeholder="••••••••"
125+
>
126+
<div id="login-error" class="status-message error" style="display: none;"></div>
127+
</div>
128+
<div class="modal-actions">
129+
<button id="login-confirm" class="btn-primary">Login</button>
130+
<button id="login-cancel" class="btn-secondary">Cancel</button>
131+
</div>
132+
</div>
133+
</div>
134+
99135
<script src="/admin.js"></script>
100136
</body>
101137
</html>

0 commit comments

Comments
 (0)