From a449a707243eefc755877d51b93ddcfd3822c7e6 Mon Sep 17 00:00:00 2001 From: Markbeep Date: Sat, 12 Apr 2025 20:01:05 +0200 Subject: [PATCH] add option to configure base url --- .env.local | 1 + README.md | 1 + app/internal/env_settings.py | 1 + app/main.py | 11 +++--- app/routers/auth.py | 8 ++--- app/routers/root.py | 7 ++-- app/routers/wishlist.py | 4 +-- app/util/redirect.py | 23 ++++++++++++ app/util/templates.py | 1 + templates/base.html | 42 ++++++++++++++-------- templates/init.html | 2 +- templates/invalid_oidc.html | 2 +- templates/login.html | 2 +- templates/manual.html | 2 +- templates/scripts/alpinejs.html | 2 +- templates/scripts/toast.html | 4 +-- templates/search.html | 9 ++--- templates/settings_page/account.html | 2 +- templates/settings_page/base.html | 14 ++++---- templates/settings_page/download.html | 15 ++++---- templates/settings_page/indexers.html | 2 +- templates/settings_page/notifications.html | 10 +++--- templates/settings_page/prowlarr.html | 6 ++-- templates/settings_page/security.html | 6 ++-- templates/settings_page/users.html | 6 ++-- templates/wishlist_page/base_wishlist.html | 6 ++-- templates/wishlist_page/manual.html | 7 ++-- templates/wishlist_page/sources.html | 6 ++-- templates/wishlist_page/wishlist.html | 16 ++++----- 29 files changed, 133 insertions(+), 85 deletions(-) create mode 100644 app/util/redirect.py diff --git a/.env.local b/.env.local index e2fcf29..288251d 100644 --- a/.env.local +++ b/.env.local @@ -2,3 +2,4 @@ ABR_APP__CONFIG_DIR=config # Path to the config directory. Default: /config ABR_APP__DEBUG=true # Default: false ABR_APP__OPENAPI_ENABLED=true # Default: false ABR_APP__LOG_LEVEL=DEBUG +ABR_APP__BASE_URL= diff --git a/README.md b/README.md index 992991a..435c8d6 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ spec: | `ABR_APP__OPENAPI_ENABLED` | If set to `true`, enables an OpenAPI specs page on `/docs`. | false | | `ABR_APP__CONFIG_DIR` | The directory path where persistant data and configuration is stored. If ran using Docker or Kubernetes, this is the location a volume should be mounted to. | /config | | `ABR_APP__LOG_LEVEL` | One of `DEBUG`, `INFO`, `WARN`, `ERROR`. | INFO | +| `ABR_APP__BASE_URL` | Defines the base url the website is hosted at. If the website is accessed at `example.org/abr/`, set the base URL to `/abr/` | | | `ABR_DB__SQLITE_PATH` | If relative, path and name of the sqlite database in relation to `ABR_APP__CONFIG_DIR`. If absolute (path starts with `/`), the config dir is ignored and only the absolute path is used. | db.sqlite | --- diff --git a/app/internal/env_settings.py b/app/internal/env_settings.py index 0c6b8a9..11373b1 100644 --- a/app/internal/env_settings.py +++ b/app/internal/env_settings.py @@ -15,6 +15,7 @@ class ApplicationSettings(BaseModel): port: int = 8000 version: str = "local" log_level: str = "INFO" + base_url: str = "" class Settings(BaseSettings): diff --git a/app/main.py b/app/main.py index 271df7e..6f70d57 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,6 @@ from urllib.parse import quote_plus, urlencode from fastapi import FastAPI, HTTPException, Request, status from fastapi.middleware import Middleware from fastapi.middleware.gzip import GZipMiddleware -from fastapi.responses import RedirectResponse from sqlalchemy import func from sqlmodel import select @@ -19,6 +18,7 @@ from app.internal.env_settings import Settings from app.internal.models import User from app.routers import auth, root, search, settings, wishlist from app.util.db import open_session +from app.util.redirect import BaseUrlRedirectResponse from app.util.templates import templates from app.util.toast import ToastException from app.util.fetch_js import fetch_scripts @@ -46,6 +46,7 @@ app = FastAPI( Middleware(DynamicSessionMiddleware, auth_secret, middleware_linker), Middleware(GZipMiddleware), ], + root_path=Settings().app.base_url.rstrip("/"), ) app.include_router(auth.router) @@ -66,7 +67,7 @@ async def redirect_to_login(request: Request, exc: RequiresLoginException): path = request.url.path if path != "/" and not path.startswith("/login"): params["redirect_uri"] = path - return RedirectResponse("/login?" + urlencode(params)) + return BaseUrlRedirectResponse("/login?" + urlencode(params)) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -80,7 +81,7 @@ async def redirect_to_invalid_oidc(request: Request, exc: InvalidOIDCConfigurati path = "/auth/invalid-oidc" if exc.detail: path += f"?error={quote_plus(exc.detail)}" - return RedirectResponse(path) + return BaseUrlRedirectResponse(path) @app.exception_handler(ToastException) @@ -116,10 +117,10 @@ async def redirect_to_init(request: Request, call_next: Any): with open_session() as session: user_count = session.exec(select(func.count()).select_from(User)).one() if user_count == 0: - return RedirectResponse("/init") + return BaseUrlRedirectResponse("/init") else: user_exists = True elif user_exists and request.url.path.startswith("/init"): - return RedirectResponse("/") + return BaseUrlRedirectResponse("/") response = await call_next(request) return response diff --git a/app/routers/auth.py b/app/routers/auth.py index 368f3ea..5aa6217 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -8,7 +8,6 @@ from urllib.parse import urlencode, urljoin import jwt from aiohttp import ClientSession from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response, status -from fastapi.responses import RedirectResponse from fastapi.security import OAuth2PasswordRequestForm from sqlmodel import Session, select @@ -24,6 +23,7 @@ from app.internal.auth.oidc_config import InvalidOIDCConfiguration, oidc_config from app.internal.models import GroupEnum, User from app.util.connection import get_connection from app.util.db import get_session +from app.util.redirect import BaseUrlRedirectResponse from app.util.templates import templates from app.util.toast import ToastException @@ -41,14 +41,14 @@ async def login( ): login_type = auth_config.get(session, "login_type") if login_type in [LoginTypeEnum.basic, LoginTypeEnum.none]: - return RedirectResponse(redirect_uri) + return BaseUrlRedirectResponse(redirect_uri) if login_type != LoginTypeEnum.oidc and backup: backup = False try: await get_authenticated_user()(request, session) # already logged in - return RedirectResponse(redirect_uri) + return BaseUrlRedirectResponse(redirect_uri) except (HTTPException, RequiresLoginException): pass @@ -91,7 +91,7 @@ async def login( "scope": scope, "state": state, } - return RedirectResponse(f"{authorize_endpoint}?" + urlencode(params)) + return BaseUrlRedirectResponse(f"{authorize_endpoint}?" + urlencode(params)) @router.post("/logout") diff --git a/app/routers/root.py b/app/routers/root.py index eb5d25a..356160a 100644 --- a/app/routers/root.py +++ b/app/routers/root.py @@ -5,7 +5,7 @@ from typing import Annotated, Callable from urllib.parse import urlencode from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response -from fastapi.responses import FileResponse, RedirectResponse +from fastapi.responses import FileResponse from sqlmodel import Session from app.internal.auth.authentication import ( @@ -18,6 +18,7 @@ from app.internal.auth.config import LoginTypeEnum, auth_config from app.internal.env_settings import Settings from app.internal.models import GroupEnum from app.util.db import get_session +from app.util.redirect import BaseUrlRedirectResponse from app.util.templates import templates router = APIRouter() @@ -129,7 +130,7 @@ def read_root( request: Request, user: Annotated[DetailedUser, Depends(get_authenticated_user())], ): - return RedirectResponse("/search") + return BaseUrlRedirectResponse("/search") # TODO: create a root page # return templates.TemplateResponse( # "root.html", @@ -179,4 +180,4 @@ def create_init( @router.get("/login") def redirect_login(request: Request): - return RedirectResponse("/auth/login?" + urlencode(request.query_params)) + return BaseUrlRedirectResponse("/auth/login?" + urlencode(request.query_params)) diff --git a/app/routers/wishlist.py b/app/routers/wishlist.py index d128922..b79cbbe 100644 --- a/app/routers/wishlist.py +++ b/app/routers/wishlist.py @@ -12,7 +12,6 @@ from fastapi import ( Request, Response, ) -from fastapi.responses import RedirectResponse from sqlmodel import Session, asc, col, select from app.internal.models import ( @@ -30,6 +29,7 @@ from app.internal.query import query_sources from app.internal.auth.authentication import DetailedUser, get_authenticated_user from app.util.connection import get_connection from app.util.db import get_session, open_session +from app.util.redirect import BaseUrlRedirectResponse from app.util.templates import template_response router = APIRouter(prefix="/wishlist") @@ -239,7 +239,7 @@ async def list_sources( try: prowlarr_config.raise_if_invalid(session) except ProwlarrMisconfigured: - return RedirectResponse( + return BaseUrlRedirectResponse( "/settings/prowlarr?prowlarr_misconfigured=1", status_code=302 ) diff --git a/app/util/redirect.py b/app/util/redirect.py new file mode 100644 index 0000000..9c8d7f9 --- /dev/null +++ b/app/util/redirect.py @@ -0,0 +1,23 @@ +from fastapi.responses import RedirectResponse +from starlette.datastructures import URL + +from app.internal.env_settings import Settings + + +class BaseUrlRedirectResponse(RedirectResponse): + """ + Redirects while preserving the base URL + """ + + def __init__(self, url: str | URL, status_code: int = 302) -> None: + if ( + isinstance(url, str) + and url.startswith("/") + or isinstance(url, URL) + and url.path.startswith("/") + ): + url = f"{Settings().app.base_url.rstrip('/')}{url}" + super().__init__( + url=url, + status_code=status_code, + ) diff --git a/app/util/templates.py b/app/util/templates.py index b11f731..227270c 100644 --- a/app/util/templates.py +++ b/app/util/templates.py @@ -15,6 +15,7 @@ templates.env.globals["version"] = Settings().app.version # pyright: ignore[rep templates.env.globals["json_regexp"] = ( # pyright: ignore[reportUnknownMemberType] r'^\{\s*(?:"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*(?:,\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*)*)?\}$' ) +templates.env.globals["base_url"] = Settings().app.base_url.rstrip("/") # pyright: ignore[reportUnknownMemberType] @overload diff --git a/templates/base.html b/templates/base.html index 71a5ec6..7ccbfb3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,9 +5,15 @@ {% block head %} AudioBookRequest {% endblock %} - - - + + + + diff --git a/templates/scripts/toast.html b/templates/scripts/toast.html index e5f821c..afb3cec 100644 --- a/templates/scripts/toast.html +++ b/templates/scripts/toast.html @@ -1,11 +1,11 @@ + +