Skip to content

Commit c3e4db6

Browse files
committed
Add custom logger option
Set env vars via pydantic BaseModel
1 parent 9dcbe0b commit c3e4db6

File tree

8 files changed

+96
-35
lines changed

8 files changed

+96
-35
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ Licensed under the [MIT License][license]
100100
[label-actions-pypi]: https://github.com/thevickypedia/FastAPI-UI-Auth/actions/workflows/python-publish.yml/badge.svg
101101
[label-pypi]: https://img.shields.io/pypi/v/FastAPI-UI-Auth
102102
[label-pypi-format]: https://img.shields.io/pypi/format/FastAPI-UI-Auth
103-
[label-pypi-status]: https://img.shields.io/pypi/status/FastAPI-UI-Auth
104-
[label-pypi-package]: https://img.shields.io/badge/Pypi%20Package-FastAPI-UI-Auth-blue?style=for-the-badge&logo=Python
103+
[label-pypi-status]: https://img.shields.io/pypi/status/FastAPI_UI_Auth
104+
[label-pypi-package]: https://img.shields.io/badge/Pypi%20Package-FastAPI_UI_Auth-blue?style=for-the-badge&logo=Python
105105
[label-pyversion]: https://img.shields.io/badge/python-3.11%20%7C%203.12-blue
106106
[label-platform]: https://img.shields.io/badge/Platform-Linux|macOS|Windows-1f425f.svg
107107
[release-notes]: https://github.com/thevickypedia/FastAPI-UI-Auth/blob/main/release_notes.rst

uiauth/logger.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Default logger for FastAPI-UI-Auth package."""
2+
3+
import logging
4+
import sys
5+
6+
CUSTOM_LOGGER = logging.getLogger(__name__)
7+
CUSTOM_LOGGER.setLevel(logging.DEBUG)
8+
CONSOLE_HANDLER = logging.StreamHandler(sys.stdout)
9+
CONSOLE_FORMATTER = logging.Formatter(
10+
fmt="%(levelname)-9s %(message)s",
11+
)
12+
CONSOLE_HANDLER.setFormatter(fmt=CONSOLE_FORMATTER)
13+
CUSTOM_LOGGER.addHandler(hdlr=CONSOLE_HANDLER)

uiauth/models.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,66 @@
1+
import os
12
import pathlib
23
from typing import Callable, Dict, List, Optional, Type
34

45
from fastapi.routing import APIRoute, APIWebSocketRoute
56
from fastapi.templating import Jinja2Templates
6-
from pydantic import BaseModel, Field
7+
from pydantic import BaseModel, Field, ValidationInfo, field_validator
78

89
from uiauth.enums import APIMethods
910

1011
templates = Jinja2Templates(directory=pathlib.Path(__file__).parent / "templates")
1112

1213

14+
def get_env(keys: List[str], default: Optional[str] = None) -> Optional[str]:
15+
"""Get environment variable value.
16+
17+
Args:
18+
keys: List of environment variable names to check.
19+
default: Default value if the environment variable is not set.
20+
21+
Returns:
22+
Value of the environment variable or default value.
23+
"""
24+
for key in keys:
25+
if value := os.getenv(key):
26+
return value
27+
if value := os.getenv(key.upper()):
28+
return value
29+
if value := os.getenv(key.lower()):
30+
return value
31+
return default
32+
33+
34+
class EnvConfig(BaseModel):
35+
"""Configuration for environment variables."""
36+
37+
username: str
38+
password: str
39+
40+
# noinspection PyMethodParameters
41+
@field_validator("username", "password", mode="before")
42+
def load_user(cls, key: str, field: ValidationInfo) -> str | None:
43+
"""Load environment variables into the configuration.
44+
45+
Args:
46+
key: Environment variable key to check.
47+
field: Field information for validation.
48+
49+
See Also:
50+
- This method checks if the environment variable is set and returns its value.
51+
- If the key is not set, it attempts to get the value from the environment using a helper function.
52+
53+
Returns:
54+
str | None:
55+
Value of the environment variable or None if not set.
56+
"""
57+
if not key:
58+
return get_env([field.field_name, field.field_name[:4]])
59+
60+
61+
env = EnvConfig
62+
63+
1364
class Parameters(BaseModel):
1465
"""Parameters for the Authenticator class.
1566

uiauth/service.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import os
32
from threading import Timer
43
from typing import Dict, List
54

@@ -13,10 +12,9 @@
1312
from fastapi.routing import APIRoute, APIWebSocketRoute
1413
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1514

16-
from uiauth import endpoints, enums, models, utils
15+
from uiauth import endpoints, enums, logger, models, utils
1716

1817
dotenv.load_dotenv(dotenv_path=dotenv.find_dotenv(), override=True)
19-
LOGGER = logging.getLogger("uvicorn.default")
2018
BEARER_AUTH = HTTPBearer()
2119

2220

@@ -33,10 +31,11 @@ def __init__(
3331
app: FastAPI,
3432
params: models.Parameters | List[models.Parameters],
3533
timeout: int = 300,
36-
username: str = os.environ.get("USERNAME"),
37-
password: str = os.environ.get("PASSWORD"),
34+
username: str = None,
35+
password: str = None,
3836
fallback_button: str = models.fallback.button,
3937
fallback_path: str = models.fallback.path,
38+
custom_logger: logging.Logger = None,
4039
):
4140
"""Initialize the APIAuthenticator with the FastAPI app and secure function.
4241
@@ -48,8 +47,9 @@ def __init__(
4847
password: Password for authentication, can be set via environment variable 'PASSWORD'.
4948
fallback_button: Title for the fallback button, defaults to "LOGIN".
5049
fallback_path: Fallback path to redirect to in case of session timeout or invalid session.
50+
custom_logger: Custom logger instance, defaults to the custom logger.
5151
"""
52-
assert all((username, password)), "'username' and 'password' are mandatory."
52+
models.env = models.EnvConfig(username=username, password=password)
5353
assert fallback_path.startswith("/"), "Fallback path must start with '/'"
5454

5555
self.app = app
@@ -68,11 +68,15 @@ def __init__(
6868
handler=utils.redirect_exception_handler,
6969
)
7070

71-
self.username = username
72-
self.password = password
71+
if custom_logger:
72+
assert isinstance(
73+
custom_logger, logging.Logger
74+
), "Custom logger must be an instance of logging.Logger"
75+
logger.CUSTOM_LOGGER = custom_logger
7376
self.timeout = timeout
7477

7578
self._secure()
79+
logger.CUSTOM_LOGGER.debug("Endpoints registered: %s", len(self.params))
7680

7781
def _verify_auth(
7882
self,
@@ -94,11 +98,11 @@ def _verify_auth(
9498
session_token = utils.verify_login(
9599
authorization=authorization,
96100
request=request,
97-
env_username=self.username,
98-
env_password=self.password,
99101
)
100102
if destination := request.cookies.get("X-Requested-By"):
101-
LOGGER.info("Setting session timeout for %s seconds", self.timeout)
103+
logger.CUSTOM_LOGGER.info(
104+
"Setting session timeout for %s seconds", self.timeout
105+
)
102106
# Set session_token cookie with a timeout, to be used for session validation when redirected
103107
response.set_cookie(
104108
key="session_token",

uiauth/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<html lang="en">
44
<head>
55
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6-
<title>FastAPI - Authenticator</title>
6+
<title>FastAPI UI Authentication</title>
77
<meta property="og:type" content="Authenticator">
88
<meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
99
<meta name="author" content="Vignesh Rao">

uiauth/templates/session.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5-
<title>FastAPI - Authenticator</title>
5+
<title>FastAPI UI Authentication</title>
66
<meta property="og:type" content="Authenticator">
77
<meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
88
<meta name="author" content="Vignesh Rao">

uiauth/templates/unauthorized.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5-
<title>FastAPI - Authenticator</title>
5+
<title>FastAPI UI Authentication</title>
66
<meta property="og:type" content="Authenticator">
77
<meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
88
<meta name="author" content="Vignesh Rao">

uiauth/utils.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
import secrets
32
from typing import List, NoReturn
43

@@ -9,9 +8,7 @@
98
from fastapi.security import HTTPAuthorizationCredentials
109
from fastapi.websockets import WebSocket
1110

12-
from uiauth import enums, models, secure
13-
14-
LOGGER = logging.getLogger("uvicorn.default")
11+
from uiauth import enums, logger, models, secure
1512

1613

1714
def failed_auth_counter(request: Request) -> None:
@@ -62,7 +59,7 @@ def raise_error(request: Request) -> NoReturn:
6259
request: Request object containing client information.
6360
"""
6461
failed_auth_counter(request)
65-
LOGGER.error(
62+
logger.CUSTOM_LOGGER.error(
6663
"Incorrect username or password: %d",
6764
models.ws_session.invalid[request.client.host],
6865
)
@@ -88,16 +85,12 @@ def extract_credentials(authorization: HTTPAuthorizationCredentials) -> List[str
8885
def verify_login(
8986
authorization: HTTPAuthorizationCredentials,
9087
request: Request,
91-
env_username: str,
92-
env_password: str,
9388
) -> str | NoReturn:
9489
"""Verifies authentication and generates session token for each user.
9590
9691
Args:
9792
authorization: Authorization header from the request.
9893
request: Request object containing client information.
99-
env_username: Environment variable for the username.
100-
env_password: Environment variable for the password.
10194
10295
Returns:
10396
str:
@@ -107,11 +100,11 @@ def verify_login(
107100
username, signature, timestamp = extract_credentials(authorization)
108101
else:
109102
raise_error(request)
110-
if secrets.compare_digest(username, env_username):
111-
hex_user = secure.hex_encode(env_username)
112-
hex_pass = secure.hex_encode(env_password)
103+
if secrets.compare_digest(username, models.env.username):
104+
hex_user = secure.hex_encode(models.env.username)
105+
hex_pass = secure.hex_encode(models.env.password)
113106
else:
114-
LOGGER.warning("User '%s' not allowed", username)
107+
logger.CUSTOM_LOGGER.warning("User '%s' not allowed", models.env.username)
115108
raise_error(request)
116109
message = f"{hex_user}{hex_pass}{timestamp}"
117110
expected_signature = secure.calculate_hash(message)
@@ -151,18 +144,18 @@ def verify_session(
151144
and session_token
152145
and secrets.compare_digest(session_token, stored_token)
153146
):
154-
LOGGER.info("Session is valid for host: %s", request.client.host)
147+
logger.CUSTOM_LOGGER.info("Session is valid for host: %s", request.client.host)
155148
return
156149
elif not session_token:
157-
LOGGER.warning(
150+
logger.CUSTOM_LOGGER.warning(
158151
"Session is invalid or expired for host: %s", request.client.host
159152
)
160153
raise models.RedirectException(
161154
source=request.url.path,
162155
destination=enums.APIEndpoints.fastapi_login,
163156
)
164157
else:
165-
LOGGER.warning(
158+
logger.CUSTOM_LOGGER.warning(
166159
"Session token mismatch for host: %s. Expected: %s, Received: %s",
167160
request.client.host,
168161
stored_token,
@@ -198,6 +191,6 @@ def clear_session(host: str) -> None:
198191
"""
199192
if models.ws_session.client_auth.get(host):
200193
models.ws_session.client_auth.pop(host)
201-
LOGGER.info("Session cleared for host: %s", host)
194+
logger.CUSTOM_LOGGER.info("Session cleared for host: %s", host)
202195
else:
203-
LOGGER.warning("No session found for host: %s", host)
196+
logger.CUSTOM_LOGGER.warning("No session found for host: %s", host)

0 commit comments

Comments
 (0)