Coverage for seedboxsync/front/login_manager.py: 91%
43 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""SeedboxSync login manager module using Flask-Login."""
9from typing import Any
10from flask import Request, Response, abort, redirect, request, session, url_for
11from flask_login import LoginManager, login_required as flask_login_required
12from seedboxsync.core.database.models import ApiKey, User
13from seedboxsync.front.babel import gettext as _
15# Setup Flask-Login
16login_manager = LoginManager()
17login_manager.login_view = "auth.login" # pyright: ignore[reportAttributeAccessIssue]
18login_manager.login_message = _("Please log in to access this page.")
19login_manager.login_message_category = "info"
22def _authenticate_by_api_key(request: Request) -> User | None:
23 """
24 Attempt to authenticate a user using an API Key from request headers.
26 Checks both the custom 'X-API-Key' header and the standard 'Authorization'
27 header formatted as a Bearer token.
29 Args:
30 request (Request): Flask request object containing incoming headers.
32 Returns:
33 User | None: Authenticated User instance if valid key is provided,
34 otherwise None.
35 """
36 headers = getattr(request, "headers", None)
38 # 1. Direct X-API-Key header
39 if headers is not None:
40 api_key_header = headers.get("X-API-Key")
41 if api_key_header:
42 return ApiKey.authenticate(api_key_header)
44 # 2. Authorization: Bearer token
45 auth = getattr(request, "authorization", None)
46 if auth and auth.type == "bearer" and getattr(auth, "token", None):
47 return ApiKey.authenticate(auth.token)
49 return None
52@login_manager.user_loader # type: ignore[untyped-decorator]
53def load_user(user_id: str) -> "User | None":
54 """
55 Retrieve and load a user instance by primary key for session management.
57 Callback used by Flask-Login to reload the user object from the user ID
58 stored in the session.
60 Args:
61 user_id (str): Unique database identifier of the user as a string.
63 Returns:
64 User | None: The matching User instance if found, or None if no record exists.
65 """
66 user = User.get_or_none(User.id == int(user_id))
67 if user is None:
68 session.clear() # Clear broken session.
69 return user
72@login_manager.unauthorized_handler # type: ignore[untyped-decorator]
73def unauthorized() -> Any | int | Response:
74 """
75 Handle unauthorized access attempts across application blueprints.
77 Differentiates between API and Web UI behavior: returns an HTTP 401 Unauthorized
78 error for API endpoints, or redirects to the login view with the original target
79 URL in the 'next' query parameter for frontend routes.
81 Returns:
82 Any | int | Response: HTTP 401 abort error for API routes, or a Flask HTTP
83 redirect response to the login page for frontend routes.
84 """
85 if request.blueprint == "api":
86 abort(401)
88 # Default behavior for the frontend
89 return redirect(url_for("auth.login", next=request.path))
92@login_manager.request_loader # type: ignore[untyped-decorator]
93def load_user_from_request(request: Request) -> User | None:
94 """
95 Load and authenticate a user from request credentials.
97 Evaluates authentication strategies in sequence:
98 1. API Key via 'X-API-Key' header or 'Bearer' token.
99 2. HTTP Basic Authentication credentials.
101 Args:
102 request (Request): Flask request object containing HTTP authorization data.
104 Returns:
105 User | None: Authenticated User instance or None if verification fails.
106 """
107 # Try API Key authentication first
108 user = _authenticate_by_api_key(request)
109 if user is not None:
110 return user
112 # Fallback to HTTP Basic authentication
113 auth = getattr(request, "authorization", None)
114 if auth and auth.type == "basic" and auth.username and auth.password:
115 try:
116 return User.authenticate(auth.username, auth.password)
117 except User.DoesNotExist: # pyright: ignore[reportAttributeAccessIssue]
118 return None
120 return None
123login_required = flask_login_required