Skip to content

Commit ecd830f

Browse files
committed
show popup when wrong password is entered
1 parent 07dcc62 commit ecd830f

5 files changed

Lines changed: 89 additions & 19 deletions

File tree

app/api/routes.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@
3636
_admin_attempts: dict[str, dict] = {}
3737

3838

39+
def _flash_html(message: str | None, level: str = "info", reason: str | None = None) -> str:
40+
if not message:
41+
return ""
42+
safe_level = level if level in {"success", "error", "warning", "info"} else "info"
43+
allowed_reasons = {"auth", "general", "success"}
44+
reason_attr = f" data-flash-reason='{reason}'" if reason in allowed_reasons else ""
45+
return (
46+
f"<div class='flash flash--{safe_level}' data-flash-level='{safe_level}'{reason_attr} role='alert'>"
47+
f"{html.escape(message)}"
48+
"</div>"
49+
)
50+
51+
3952
async def enforce_rate_limit(request: Request):
4053
client = request.client.host if request.client else "unknown"
4154
allowed, retry_after = rate_limiter.hit(client)
@@ -113,17 +126,19 @@ def _human_bytes(value: int) -> str:
113126
return "0 B"
114127

115128

116-
def _render_admin_login(message: str | None = None) -> str:
117-
flash_html = f"<div class='flash'>{html.escape(message)}</div>" if message else ""
129+
def _render_admin_login(message: str | None = None, level: str = "info", reason: str | None = None) -> str:
130+
flash_html = _flash_html(message, level, reason)
118131
return render_template("pages/admin_login.html", {"flash_message": flash_html})
119132

120133

121-
def _render_admin_page(session: Session, message: str | None = None) -> str:
134+
def _render_admin_page(
135+
session: Session, message: str | None = None, level: str = "info", reason: str | None = None
136+
) -> str:
122137
totals = fetch_storage_totals(session)
123138
snapshot = metrics.snapshot()
124139
stmt = select(FileModel).order_by(FileModel.created_at.desc()).limit(50)
125140
files = session.exec(stmt).all()
126-
flash_html = f"<div class='flash'>{html.escape(message)}</div>" if message else ""
141+
flash_html = _flash_html(message, level, reason)
127142
return render_template(
128143
"pages/admin.html",
129144
{
@@ -197,13 +212,6 @@ async def _auth_admin(request: Request, allow_blank: bool):
197212
raise HTTPException(status_code=401, detail=msg)
198213

199214

200-
async def require_admin(request: Request) -> str:
201-
success, _, _ = await _auth_admin(request, allow_blank=False)
202-
if success:
203-
return ADMIN_PASSWORD
204-
raise HTTPException(status_code=500, detail="Admin authentication failed")
205-
206-
207215
def _remove_file_from_disk(stored_name: str) -> None:
208216
try:
209217
path = (UPLOAD_ROOT / stored_name).resolve()
@@ -219,7 +227,7 @@ async def admin_dashboard(request: Request, session: Session = Depends(get_sessi
219227
if success:
220228
html = _render_admin_page(session, message)
221229
return HTMLResponse(content=html)
222-
html = _render_admin_login(message)
230+
html = _render_admin_login(message, "error" if message else "info", "auth" if message else None)
223231
status = 429 if locked and message else 200
224232
return HTMLResponse(content=html, status_code=status)
225233

@@ -228,38 +236,59 @@ async def admin_dashboard(request: Request, session: Session = Depends(get_sessi
228236
async def admin_delete_file(
229237
request: Request,
230238
session: Session = Depends(get_session),
231-
_: str = Depends(require_admin),
232239
):
233-
form = getattr(request.state, "admin_form", None) or await request.form()
240+
form = getattr(request.state, "admin_form", None)
241+
if form is None:
242+
form = await request.form()
243+
request.state.admin_form = form
244+
245+
success, message, locked = await _auth_admin(request, allow_blank=True)
246+
if not success:
247+
status = 429 if locked else 401
248+
failure_message = message or "Admin password required."
249+
html = _render_admin_page(session, failure_message, "error", "auth")
250+
return HTMLResponse(content=html, status_code=status)
251+
234252
file_id = form.get("file_id")
235253
if not file_id:
236254
raise HTTPException(status_code=400, detail="Missing file_id")
237255
file = session.get(FileModel, file_id)
238256
if not file:
239-
html = _render_admin_page(session, "File not found.")
257+
html = _render_admin_page(session, "File not found.", "error", "general")
240258
return HTMLResponse(content=html, status_code=404)
241259

242260
_remove_file_from_disk(file.stored_name)
243261
session.delete(file)
244262
session.commit()
245-
html = _render_admin_page(session, "File deleted.")
263+
html = _render_admin_page(session, "File deleted.", "success", "success")
246264
return HTMLResponse(content=html)
247265

248266

249267
@router.post("/admin/delete-all", response_class=HTMLResponse)
250268
async def admin_delete_all(
251269
request: Request,
252270
session: Session = Depends(get_session),
253-
_: str = Depends(require_admin),
254271
):
272+
form = getattr(request.state, "admin_form", None)
273+
if form is None:
274+
form = await request.form()
275+
request.state.admin_form = form
276+
277+
success, message, locked = await _auth_admin(request, allow_blank=True)
278+
if not success:
279+
status = 429 if locked else 401
280+
failure_message = message or "Admin password required."
281+
html = _render_admin_page(session, failure_message, "error", "auth")
282+
return HTMLResponse(content=html, status_code=status)
283+
255284
files = session.exec(select(FileModel)).all()
256285
deleted = 0
257286
for file in files:
258287
_remove_file_from_disk(file.stored_name)
259288
session.delete(file)
260289
deleted += 1
261290
session.commit()
262-
html = _render_admin_page(session, f"Deleted {deleted} files.")
291+
html = _render_admin_page(session, f"Deleted {deleted} files.", "success", "success")
263292
return HTMLResponse(content=html)
264293

265294

app/static/css/admin.css

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,24 @@ tbody tr:last-child td {
124124
margin: 1rem 0;
125125
padding: 1rem;
126126
border-radius: 10px;
127+
border: 1px solid rgba(148, 163, 184, 0.35);
128+
background: rgba(148, 163, 184, 0.15);
129+
}
130+
.flash--success {
127131
background: rgba(34, 197, 94, 0.15);
128-
border: 1px solid rgba(34, 197, 94, 0.3);
132+
border-color: rgba(34, 197, 94, 0.4);
133+
}
134+
.flash--error {
135+
background: rgba(239, 68, 68, 0.15);
136+
border-color: rgba(239, 68, 68, 0.45);
137+
}
138+
.flash--warning {
139+
background: rgba(250, 204, 21, 0.15);
140+
border-color: rgba(250, 204, 21, 0.4);
141+
}
142+
.flash--info {
143+
background: rgba(59, 130, 246, 0.15);
144+
border-color: rgba(59, 130, 246, 0.4);
129145
}
130146
.danger {
131147
margin-top: 2rem;

app/static/js/admin.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(function () {
2+
const run = () => {
3+
const flash = document.querySelector(".flash[data-flash-reason='auth']");
4+
if (!flash) {
5+
return;
6+
}
7+
const message = flash.textContent ? flash.textContent.trim() : "";
8+
if (!message) {
9+
return;
10+
}
11+
if (flash.dataset.popupShown === "1") {
12+
return;
13+
}
14+
flash.dataset.popupShown = "1";
15+
window.alert(message);
16+
};
17+
18+
if (document.readyState === "loading") {
19+
document.addEventListener("DOMContentLoaded", run, { once: true });
20+
} else {
21+
run();
22+
}
23+
})();

app/templates/pages/admin.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@ <h2>Danger Zone</h2>
5858
<footer>
5959
<a href="/">Back to Home</a>
6060
</footer>
61+
<script src="/static/js/admin.js"></script>
6162
</body>
6263
</html>

app/templates/pages/admin_login.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ <h1>Admin Access</h1>
1616
<button type="submit">Enter dashboard</button>
1717
</form>
1818
</main>
19+
<script src="/static/js/admin.js"></script>
1920
</body>
2021
</html>

0 commit comments

Comments
 (0)