Files
alembic/app/routers/auth.py
T
andrew 2c86d7973f P2 minor set: password off argv, configurable share gate, POST logout, log path check
- Navidrome password is now passed to curl via stdin (--data-urlencode "p@-")
  in navidrome-scan.sh and pipeline-status.sh, so it no longer appears in
  ps/proc. Verified the query sent is identical and a live scan still triggers.
- MIN_ARTIST_DIRS (the share-health gate) is now a setting, threaded through to
  the pipeline env, so a user with a small library can lower it instead of the
  scan/sync being permanently blocked by the hardcoded 500.
- /auth/logout is now POST-only (with a nav form + aria-label), so a drive-by
  GET can't log the user out; enforced allowed_email already landed separately.
- view_log now confirms the run's log_path resolves under the logs dir before
  serving it (defense in depth).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:50:06 -06:00

67 lines
2.4 KiB
Python

import time
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import select
from app.db import SessionLocal
from app.models import User
from app.security.oidc import oauth
from app.services import diagnostics
from app.settings import settings
router = APIRouter(prefix="/auth", tags=["auth"])
@router.get("/login")
async def login(request: Request):
# If OIDC isn't configured, the client was never registered and calling it
# would raise an opaque 500. Send the operator to the setup check instead,
# which spells out exactly which variables are missing.
if not diagnostics.oidc_configured():
return RedirectResponse(url="/setup")
redirect_uri = request.url_for("auth_callback")
return await oauth.pocketid.authorize_redirect(request, redirect_uri)
@router.get("/callback", name="auth_callback")
async def auth_callback(request: Request):
token = await oauth.pocketid.authorize_access_token(request)
userinfo = token.get("userinfo") or await oauth.pocketid.userinfo(token=token)
sub = userinfo["sub"]
email = userinfo.get("email")
name = userinfo.get("name")
now = time.time()
# Enforce the allowlist here, before creating any user row or session, so a
# disallowed identity never gets persisted or a cookie (require_auth also
# checks it on every request, but that's after the fact).
if settings.allowed_email and email != settings.allowed_email:
raise HTTPException(status_code=403, detail="This account is not authorized to use alembic.")
with SessionLocal() as db:
user = db.execute(select(User).where(User.oidc_sub == sub)).scalar_one_or_none()
if user is None:
user = User(oidc_sub=sub, email=email, name=name, last_login_at=now)
db.add(user)
else:
user.email = email
user.name = name
user.last_login_at = now
db.commit()
request.session["user"] = {"sub": sub, "email": email, "name": name}
return RedirectResponse(url="/")
@router.post("/logout")
async def logout(request: Request):
request.session.clear()
end_session_endpoint = None
metadata = await oauth.pocketid.load_server_metadata()
end_session_endpoint = metadata.get("end_session_endpoint")
if end_session_endpoint:
return RedirectResponse(url=end_session_endpoint)
return RedirectResponse(url="/")