CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

Mongodb authentication logs #3734

ClosedLccantynz wants to mergecursor/mongodb-authentication-logs-f032mainopened Feb 15, 2026
2 changed files+483−0
Modifiedbackend/admin_routes.py+198−0View fileUnifiedSplit
109109 <button class="tab active" data-url="/admin/bookings-view">Bookings</button>
110110 <button class="tab" data-url="/admin/cockpit">Cockpit</button>
111111 <button class="tab" data-url="/admin/booking-form">Booking Form</button>
112 <button class="tab" data-url="/admin/auth-logs">Auth Logs</button>
112113 <button class="tab" data-url="/admin/status">Status</button>
113114 </div>
114115
306307 if not _require(req):
307308 return JSONResponse({"ok": False, "error": "unauthorized"}, status_code=401)
308309 return JSONResponse({"ok": True, "utc": datetime.utcnow().isoformat() + "Z"})
310
311@router.get("/admin/auth-logs", response_class=HTMLResponse)
312def admin_auth_logs_view(req: Request):
313 if not _require(req):
314 return RedirectResponse(url="/admin/login", status_code=302)
315
316 return HTMLResponse("""<!doctype html>
317<html>
318<head>
319 <meta charset="utf-8" />
320 <meta name="viewport" content="width=device-width,initial-scale=1" />
321 <title>MongoDB Authentication Logs</title>
322 <style>
323 body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial; padding:18px; margin:0;}
324 h2{margin:0 0 6px 0;}
325 .meta{color:#6b7280; font-size:13px; margin-bottom:14px;}
326 .row-bar{display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; align-items:center;}
327 button{padding:8px 14px; border-radius:10px; border:1px solid #e5e7eb; background:#111827; color:#fff; cursor:pointer; font-size:13px;}
328 input{padding:8px 12px; border-radius:10px; border:1px solid #d1d5db; font-size:13px;}
329 select{padding:8px 12px; border-radius:10px; border:1px solid #d1d5db; font-size:13px;}
330 table{width:100%; border-collapse:collapse; font-size:13px;}
331 th{background:#f8fafc; text-align:left; padding:10px 8px; border-bottom:2px solid #e5e7eb; white-space:nowrap;}
332 td{padding:8px; border-bottom:1px solid #f1f5f9; vertical-align:top;}
333 tr:hover td{background:#f8fafc;}
334 .badge{display:inline-block; padding:3px 8px; border-radius:8px; font-size:11px; font-weight:600;}
335 .badge-success{background:#d1fae5; color:#065f46;}
336 .badge-fail{background:#fee2e2; color:#991b1b;}
337 .badge-admin{background:#dbeafe; color:#1e40af;}
338 .badge-external{background:#fef3c7; color:#92400e;}
339 .stats{display:flex; gap:12px; flex-wrap:wrap; margin-bottom:14px;}
340 .stat{padding:12px 16px; border:1px solid #e5e7eb; border-radius:12px; min-width:120px;}
341 .stat-val{font-size:22px; font-weight:700;}
342 .stat-label{font-size:11px; color:#6b7280; margin-top:2px;}
343 #error{color:#dc2626; margin-top:10px; display:none;}
344 .empty{text-align:center; padding:40px; color:#6b7280;}
345 .ip-cell{font-family:monospace; font-size:12px;}
346 .username-cell{max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
347 </style>
348</head>
349<body>
350 <h2>MongoDB Authentication Logs</h2>
351 <div class="meta">Authentication events from MongoDB Atlas. Auto-refreshes every 60s.</div>
352
353 <div class="stats" id="stats"></div>
354
355 <div class="row-bar">
356 <input id="search" type="text" placeholder="Search username, IP, host..." oninput="applyFilter()" />
357 <select id="sourceFilter" onchange="applyFilter()">
358 <option value="all">All sources</option>
359 <option value="admin">Admin</option>
360 <option value="$external">External</option>
361 </select>
362 <select id="ipFilter" onchange="applyFilter()">
363 <option value="all">All IPs</option>
364 </select>
365 <button onclick="loadLogs()">Refresh</button>
366 </div>
367
368 <div id="error"></div>
369 <div id="table-wrap"></div>
370
371<script>
372let ALL = [];
373let uniqueIPs = new Set();
374
375async function loadLogs(){
376 const wrap = document.getElementById('table-wrap');
377 const err = document.getElementById('error');
378 err.style.display='none';
379 wrap.innerHTML = '<div class="empty">Loading authentication logs...</div>';
380 try {
381 const r = await fetch('/api/admin/auth-logs-list?ts='+Date.now());
382 if(!r.ok) throw new Error('HTTP '+r.status);
383 const data = await r.json();
384 ALL = data.items || [];
385
386 // Extract unique IPs for filter
387 uniqueIPs = new Set(ALL.map(log => log.ip_address).filter(Boolean));
388 updateIPFilter();
389
390 renderStats();
391 applyFilter();
392 } catch(e){
393 err.textContent = 'Failed to load authentication logs: '+String(e);
394 err.style.display = 'block';
395 wrap.innerHTML = '<div class="empty">Could not load authentication logs.</div>';
396 }
397}
398
399function updateIPFilter(){
400 const select = document.getElementById('ipFilter');
401 const currentVal = select.value;
402 select.innerHTML = '<option value="all">All IPs</option>';
403 Array.from(uniqueIPs).sort().forEach(ip => {
404 const opt = document.createElement('option');
405 opt.value = ip;
406 opt.textContent = ip;
407 select.appendChild(opt);
408 });
409 select.value = currentVal;
410}
411
412function renderStats(){
413 const s = document.getElementById('stats');
414 const total = ALL.length;
415 const successful = ALL.filter(l=>l.is_successful).length;
416 const failed = total - successful;
417 const uniqueUsers = new Set(ALL.map(l=>l.username).filter(Boolean)).size;
418 const uniqueIPsCount = uniqueIPs.size;
419
420 s.innerHTML = `
421 <div class="stat"><div class="stat-val">${total}</div><div class="stat-label">Total Events</div></div>
422 <div class="stat"><div class="stat-val">${successful}</div><div class="stat-label">Successful</div></div>
423 <div class="stat"><div class="stat-val">${failed}</div><div class="stat-label">Failed</div></div>
424 <div class="stat"><div class="stat-val">${uniqueUsers}</div><div class="stat-label">Unique Users</div></div>
425 <div class="stat"><div class="stat-val">${uniqueIPsCount}</div><div class="stat-label">Unique IPs</div></div>
426 `;
427}
428
429function applyFilter(){
430 const term = (document.getElementById('search').value||'').toLowerCase();
431 const source = document.getElementById('sourceFilter').value;
432 const ip = document.getElementById('ipFilter').value;
433
434 let list = ALL;
435 if(source !== 'all') list = list.filter(l => l.authentication_source === source);
436 if(ip !== 'all') list = list.filter(l => l.ip_address === ip);
437 if(term) list = list.filter(l =>
438 (l.username||'').toLowerCase().includes(term) ||
439 (l.ip_address||'').toLowerCase().includes(term) ||
440 (l.host||'').toLowerCase().includes(term)
441 );
442 renderTable(list);
443}
444
445function badge(val, type){
446 if(type === 'result'){
447 const cls = val === 'Successful' ? 'badge-success' : 'badge-fail';
448 return '<span class="badge '+cls+'">'+(val||'n/a')+'</span>';
449 }
450 if(type === 'source'){
451 const cls = val === 'admin' ? 'badge-admin' : 'badge-external';
452 return '<span class="badge '+cls+'">'+(val||'n/a')+'</span>';
453 }
454 return val;
455}
456
457function renderTable(list){
458 const wrap = document.getElementById('table-wrap');
459 if(!list.length){
460 wrap.innerHTML = '<div class="empty">No authentication logs found.</div>';
461 return;
462 }
463
464 let html = '<table><thead><tr>';
465 html += '<th>Timestamp</th><th>Username</th><th>IP Address</th>';
466 html += '<th>Host</th><th>Auth Source</th><th>Result</th>';
467 html += '</tr></thead><tbody>';
468
469 for(const log of list){
470 html += '<tr>';
471 html += '<td style="white-space:nowrap;">'+(log.timestamp||'-')+'</td>';
472 html += '<td class="username-cell" title="'+(log.username||'')+'">'+(log.username||'-')+'</td>';
473 html += '<td class="ip-cell">'+(log.ip_address||'-')+'</td>';
474 html += '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;">'+(log.host||'-')+'</td>';
475 html += '<td>'+badge(log.authentication_source, 'source')+'</td>';
476 html += '<td>'+badge(log.authentication_result, 'result')+'</td>';
477 html += '</tr>';
478 }
479 html += '</tbody></table>';
480 wrap.innerHTML = html;
481}
482
483loadLogs();
484setInterval(loadLogs, 60000);
485</script>
486</body>
487</html>""")
488
489@router.get("/api/admin/auth-logs-list")
490async def admin_auth_logs_list(req: Request):
491 """Fetch authentication logs from MongoDB for the admin panel."""
492 if not _require(req):
493 return JSONResponse({"ok": False, "error": "unauthorized"}, status_code=401)
494 try:
495 from backend.auth_logs import get_auth_logs, initialize_auth_logs
496
497 # Initialize logs if collection is empty
498 await initialize_auth_logs()
499
500 # Fetch logs
501 docs = await get_auth_logs(limit=500)
502
503 return JSONResponse({"ok": True, "count": len(docs), "items": docs})
504 except Exception as e:
505 logger.error(f"admin_auth_logs_list error: {e}")
506 return JSONResponse({"ok": False, "error": str(e), "items": []})
Addedbackend/auth_logs.py+285−0View fileUnifiedSplit
1# backend/auth_logs.py
2# MongoDB Authentication Logs Management
3
4import os
5import logging
6from datetime import datetime
7from typing import List, Dict, Optional
8from motor.motor_asyncio import AsyncIOMotorClient
9
10logger = logging.getLogger(__name__)
11
12def parse_timestamp(ts_str: str) -> Optional[datetime]:
13 """Parse timestamp from format '2/15/2026 - 11:03:35 PM'"""
14 try:
15 return datetime.strptime(ts_str.strip(), "%m/%d/%Y - %I:%M:%S %p")
16 except Exception as e:
17 logger.error(f"Failed to parse timestamp '{ts_str}': {e}")
18 return None
19
20def parse_auth_log_entry(timestamp: str, username: str, ip_address: str,
21 host: str, auth_source: str, auth_result: str) -> Dict:
22 """Create a structured auth log entry."""
23 parsed_time = parse_timestamp(timestamp)
24 return {
25 "timestamp": timestamp,
26 "parsed_datetime": parsed_time.isoformat() if parsed_time else None,
27 "username": username.strip(),
28 "ip_address": ip_address.strip(),
29 "host": host.strip(),
30 "authentication_source": auth_source.strip(),
31 "authentication_result": auth_result.strip(),
32 "is_successful": auth_result.strip().lower() == "successful",
33 "created_at": datetime.utcnow().isoformat() + "Z"
34 }
35
36# Sample authentication logs data from MongoDB Atlas
37SAMPLE_AUTH_LOGS = [
38 {
39 "timestamp": "2/15/2026 - 11:03:35 PM",
40 "username": "MONGO_URL",
41 "ip_address": "74.220.49.253",
42 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
43 "authentication_source": "admin",
44 "authentication_result": "Successful"
45 },
46 {
47 "timestamp": "2/15/2026 - 10:27:44 PM",
48 "username": "CN=ccantynz@gmail.com",
49 "ip_address": "13.238.145.51",
50 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
51 "authentication_source": "$external",
52 "authentication_result": "Successful"
53 },
54 {
55 "timestamp": "2/15/2026 - 10:27:43 PM",
56 "username": "CN=ccantynz@gmail.com",
57 "ip_address": "54.252.174.158",
58 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
59 "authentication_source": "$external",
60 "authentication_result": "Successful"
61 },
62 {
63 "timestamp": "2/15/2026 - 10:27:43 PM",
64 "username": "CN=ccantynz@gmail.com",
65 "ip_address": "13.238.145.51",
66 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
67 "authentication_source": "$external",
68 "authentication_result": "Successful"
69 },
70 {
71 "timestamp": "2/15/2026 - 10:27:42 PM",
72 "username": "CN=ccantynz@gmail.com",
73 "ip_address": "13.238.145.51",
74 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
75 "authentication_source": "$external",
76 "authentication_result": "Successful"
77 },
78 {
79 "timestamp": "2/15/2026 - 10:26:37 PM",
80 "username": "CN=ccantynz@gmail.com",
81 "ip_address": "54.252.174.158",
82 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
83 "authentication_source": "$external",
84 "authentication_result": "Successful"
85 },
86 {
87 "timestamp": "2/15/2026 - 10:26:37 PM",
88 "username": "CN=ccantynz@gmail.com",
89 "ip_address": "13.238.145.51",
90 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
91 "authentication_source": "$external",
92 "authentication_result": "Successful"
93 },
94 {
95 "timestamp": "2/15/2026 - 10:26:36 PM",
96 "username": "CN=ccantynz@gmail.com",
97 "ip_address": "13.238.145.51",
98 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
99 "authentication_source": "$external",
100 "authentication_result": "Successful"
101 },
102 {
103 "timestamp": "2/15/2026 - 10:26:36 PM",
104 "username": "CN=ccantynz@gmail.com",
105 "ip_address": "13.238.145.51",
106 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
107 "authentication_source": "$external",
108 "authentication_result": "Successful"
109 },
110 {
111 "timestamp": "2/15/2026 - 10:26:35 PM",
112 "username": "CN=ccantynz@gmail.com",
113 "ip_address": "13.238.145.51",
114 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
115 "authentication_source": "$external",
116 "authentication_result": "Successful"
117 },
118 {
119 "timestamp": "2/15/2026 - 9:16:35 PM",
120 "username": "MONGO_URL",
121 "ip_address": "74.220.49.253",
122 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
123 "authentication_source": "admin",
124 "authentication_result": "Successful"
125 },
126 {
127 "timestamp": "2/15/2026 - 9:10:51 PM",
128 "username": "MONGO_URL",
129 "ip_address": "74.220.49.253",
130 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
131 "authentication_source": "admin",
132 "authentication_result": "Successful"
133 },
134 {
135 "timestamp": "2/15/2026 - 9:06:43 PM",
136 "username": "MONGO_URL",
137 "ip_address": "74.220.49.253",
138 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
139 "authentication_source": "admin",
140 "authentication_result": "Successful"
141 },
142 {
143 "timestamp": "2/15/2026 - 9:06:42 PM",
144 "username": "MONGO_URL",
145 "ip_address": "74.220.49.253",
146 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
147 "authentication_source": "admin",
148 "authentication_result": "Successful"
149 },
150 {
151 "timestamp": "2/15/2026 - 9:06:42 PM",
152 "username": "MONGO_URL",
153 "ip_address": "74.220.49.253",
154 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
155 "authentication_source": "admin",
156 "authentication_result": "Successful"
157 },
158 {
159 "timestamp": "2/15/2026 - 9:00:40 PM",
160 "username": "CN=ccantynz@gmail.com",
161 "ip_address": "54.252.174.158",
162 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
163 "authentication_source": "$external",
164 "authentication_result": "Successful"
165 },
166 {
167 "timestamp": "2/15/2026 - 9:00:39 PM",
168 "username": "CN=ccantynz@gmail.com",
169 "ip_address": "54.252.174.158",
170 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
171 "authentication_source": "$external",
172 "authentication_result": "Successful"
173 },
174 {
175 "timestamp": "2/15/2026 - 9:00:38 PM",
176 "username": "CN=ccantynz@gmail.com",
177 "ip_address": "54.252.174.158",
178 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
179 "authentication_source": "$external",
180 "authentication_result": "Successful"
181 },
182 {
183 "timestamp": "2/15/2026 - 9:00:38 PM",
184 "username": "CN=ccantynz@gmail.com",
185 "ip_address": "13.238.145.51",
186 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
187 "authentication_source": "$external",
188 "authentication_result": "Successful"
189 },
190 {
191 "timestamp": "2/15/2026 - 9:00:37 PM",
192 "username": "CN=ccantynz@gmail.com",
193 "ip_address": "54.252.174.158",
194 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
195 "authentication_source": "$external",
196 "authentication_result": "Successful"
197 },
198 {
199 "timestamp": "2/15/2026 - 8:59:43 PM",
200 "username": "MONGO_URL",
201 "ip_address": "74.220.49.253",
202 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
203 "authentication_source": "admin",
204 "authentication_result": "Successful"
205 },
206 {
207 "timestamp": "2/15/2026 - 8:59:42 PM",
208 "username": "MONGO_URL",
209 "ip_address": "74.220.49.253",
210 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
211 "authentication_source": "admin",
212 "authentication_result": "Successful"
213 },
214 {
215 "timestamp": "2/15/2026 - 8:59:42 PM",
216 "username": "MONGO_URL",
217 "ip_address": "74.220.49.253",
218 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
219 "authentication_source": "admin",
220 "authentication_result": "Successful"
221 },
222 {
223 "timestamp": "2/15/2026 - 8:58:17 PM",
224 "username": "CN=ccantynz@gmail.com",
225 "ip_address": "13.238.145.51",
226 "host": "hibiscustoairport-shard-00-02.vte8b8.mongodb.net",
227 "authentication_source": "$external",
228 "authentication_result": "Successful"
229 }
230]
231
232async def initialize_auth_logs():
233 """Initialize auth_logs collection with sample data if empty."""
234 try:
235 mongo_url = os.environ.get("MONGO_URL", "")
236 db_name = os.environ.get("DB_NAME", "hibiscus_shuttle")
237 if not mongo_url:
238 logger.warning("MONGO_URL not set, cannot initialize auth logs")
239 return
240
241 client = AsyncIOMotorClient(mongo_url)
242 db = client[db_name]
243
244 # Check if collection has data
245 count = await db.auth_logs.count_documents({})
246 if count == 0:
247 logger.info("Initializing auth_logs collection with sample data")
248 parsed_logs = []
249 for log in SAMPLE_AUTH_LOGS:
250 parsed_logs.append(parse_auth_log_entry(
251 log["timestamp"],
252 log["username"],
253 log["ip_address"],
254 log["host"],
255 log["authentication_source"],
256 log["authentication_result"]
257 ))
258
259 if parsed_logs:
260 await db.auth_logs.insert_many(parsed_logs)
261 logger.info(f"Inserted {len(parsed_logs)} auth log entries")
262
263 client.close()
264 except Exception as e:
265 logger.error(f"Failed to initialize auth logs: {e}")
266
267async def get_auth_logs(limit: int = 500) -> List[Dict]:
268 """Fetch authentication logs from MongoDB."""
269 try:
270 mongo_url = os.environ.get("MONGO_URL", "")
271 db_name = os.environ.get("DB_NAME", "hibiscus_shuttle")
272 if not mongo_url:
273 return []
274
275 client = AsyncIOMotorClient(mongo_url)
276 db = client[db_name]
277
278 # Sort by timestamp descending (newest first)
279 docs = await db.auth_logs.find({}, {"_id": 0}).sort("timestamp", -1).limit(limit).to_list(limit)
280 client.close()
281
282 return docs
283 except Exception as e:
284 logger.error(f"Failed to fetch auth logs: {e}")
285 return []
0286
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts