Build 2602262159: restore jellyfin-first user source

This commit is contained in:
2026-02-26 22:00:19 +13:00
parent 1c6b8255c1
commit 7257d32d6c
5 changed files with 139 additions and 34 deletions

View File

@@ -19,6 +19,7 @@ from ..db import (
set_user_password,
set_jellyfin_auth_cache,
set_user_jellyseerr_id,
set_user_auth_provider,
get_signup_invite_by_code,
get_signup_invite_by_id,
list_signup_invites,
@@ -79,6 +80,26 @@ def _prune_attempts(bucket: deque[float], now: float, window_seconds: int) -> No
bucket.popleft()
def _pick_preferred_ci_user_match(users: list[dict], requested_username: str) -> dict | None:
if not users:
return None
requested = (requested_username or "").strip()
requested_lower = requested.lower()
def _rank(user: dict) -> tuple[int, int, int, int]:
provider = str(user.get("auth_provider") or "local").strip().lower()
role = str(user.get("role") or "user").strip().lower()
username = str(user.get("username") or "")
return (
0 if role == "admin" else 1,
0 if isinstance(user.get("jellyseerr_user_id"), int) else 1,
0 if provider == "jellyfin" else (1 if provider == "local" else (2 if provider == "jellyseerr" else 3)),
0 if username.lower() == requested_lower else 1,
)
return sorted(users, key=_rank)[0]
def _record_login_failure(request: Request, username: str) -> None:
now = time.monotonic()
window = max(int(settings.auth_rate_limit_window_seconds or 60), 1)
@@ -492,13 +513,20 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
candidate_map = build_jellyseerr_candidate_map(jellyseerr_users or [])
username = form_data.username
password = form_data.password
user = get_user_by_username(username)
ci_matches = get_users_by_username_ci(username)
preferred_match = _pick_preferred_ci_user_match(ci_matches, username)
canonical_username = str(preferred_match.get("username") or username) if preferred_match else username
user = preferred_match or get_user_by_username(username)
_assert_user_can_login(user)
if user and _has_valid_jellyfin_cache(user, password):
token = create_access_token(username, "user")
token = create_access_token(canonical_username, "user")
_clear_login_failures(request, username)
set_last_login(username)
return {"access_token": token, "token_type": "bearer", "user": {"username": username, "role": "user"}}
set_last_login(canonical_username)
return {
"access_token": token,
"token_type": "bearer",
"user": {"username": canonical_username, "role": "user"},
}
try:
response = await client.authenticate_by_name(username, password)
except Exception as exc:
@@ -506,30 +534,36 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
if not isinstance(response, dict) or not response.get("User"):
_record_login_failure(request, username)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Jellyfin credentials")
create_user_if_missing(username, "jellyfin-user", role="user", auth_provider="jellyfin")
user = get_user_by_username(username)
if not preferred_match:
create_user_if_missing(canonical_username, "jellyfin-user", role="user", auth_provider="jellyfin")
elif (
user
and str(user.get("role") or "user").strip().lower() != "admin"
and str(user.get("auth_provider") or "local").strip().lower() != "jellyfin"
):
set_user_auth_provider(canonical_username, "jellyfin")
user = get_user_by_username(canonical_username)
user = get_user_by_username(canonical_username)
_assert_user_can_login(user)
try:
users = await client.get_users()
if isinstance(users, list):
save_jellyfin_users_cache(users)
for jellyfin_user in users:
if not isinstance(jellyfin_user, dict):
continue
name = jellyfin_user.get("Name")
if isinstance(name, str) and name:
create_user_if_missing(name, "jellyfin-user", role="user", auth_provider="jellyfin")
except Exception:
pass
set_jellyfin_auth_cache(username, password)
set_jellyfin_auth_cache(canonical_username, password)
if user and user.get("jellyseerr_user_id") is None and candidate_map:
matched_id = match_jellyseerr_user_id(username, candidate_map)
matched_id = match_jellyseerr_user_id(canonical_username, candidate_map)
if matched_id is not None:
set_user_jellyseerr_id(username, matched_id)
token = create_access_token(username, "user")
set_user_jellyseerr_id(canonical_username, matched_id)
token = create_access_token(canonical_username, "user")
_clear_login_failures(request, username)
set_last_login(username)
return {"access_token": token, "token_type": "bearer", "user": {"username": username, "role": "user"}}
set_last_login(canonical_username)
return {
"access_token": token,
"token_type": "bearer",
"user": {"username": canonical_username, "role": "user"},
}
@router.post("/jellyseerr/login")
@@ -548,21 +582,29 @@ async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestFor
_record_login_failure(request, form_data.username)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Jellyseerr credentials")
jellyseerr_user_id = _extract_jellyseerr_user_id(response)
create_user_if_missing(
form_data.username,
"jellyseerr-user",
role="user",
auth_provider="jellyseerr",
jellyseerr_user_id=jellyseerr_user_id,
)
user = get_user_by_username(form_data.username)
ci_matches = get_users_by_username_ci(form_data.username)
preferred_match = _pick_preferred_ci_user_match(ci_matches, form_data.username)
canonical_username = str(preferred_match.get("username") or form_data.username) if preferred_match else form_data.username
if not preferred_match:
create_user_if_missing(
canonical_username,
"jellyseerr-user",
role="user",
auth_provider="jellyseerr",
jellyseerr_user_id=jellyseerr_user_id,
)
user = get_user_by_username(canonical_username)
_assert_user_can_login(user)
if jellyseerr_user_id is not None:
set_user_jellyseerr_id(form_data.username, jellyseerr_user_id)
token = create_access_token(form_data.username, "user")
set_user_jellyseerr_id(canonical_username, jellyseerr_user_id)
token = create_access_token(canonical_username, "user")
_clear_login_failures(request, form_data.username)
set_last_login(form_data.username)
return {"access_token": token, "token_type": "bearer", "user": {"username": form_data.username, "role": "user"}}
set_last_login(canonical_username)
return {
"access_token": token,
"token_type": "bearer",
"user": {"username": canonical_username, "role": "user"},
}
@router.get("/me")