From 8a8fff37e0b08dba68d9d620970805e568f68e26 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Sat, 25 Apr 2026 05:22:21 -0500 Subject: [PATCH] Convert order/delay from path params to query params - Merged hash_page and hash_page_with_refresh into single /{file_hash} endpoint - Added optional order and delay query parameters - Updated _render_page to use _build_url for query param URLs - Updated play/pause buttons to use query param format - Removed old /{order}/{delay}/{file_hash} route --- TODO.md | 47 ++++++++++++++++++ main.py | 150 +++++++++++++++++++++++--------------------------------- 2 files changed, 108 insertions(+), 89 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7e283b3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,47 @@ +# TODO + +## Goal +Remove all authentication from the server and convert `order`/`delay` from path parameters to query parameters. + +## Tasks + +### 1. Remove authentication from `main.py` +- [x] Remove `HTTPBasic`, `HTTPBasicCredentials`, `Depends` imports (where used for auth) +- [x] Remove `security`, `expected_password`, `get_current_username`, and `set_auth_password` code +- [x] Remove `--password` CLI argument +- [x] Remove `set_auth_password(args.password)` call from `__main__` +- [x] Remove `username: str = Depends(get_current_username)` from all route handlers + +### 2. Convert `order`/`delay` to query parameters in `main.py` +- [x] Merge `hash_page` and `hash_page_with_refresh` into a single `/{file_hash}` endpoint that accepts optional query params `order` (str, default None) and `delay` (int, default None) +- [x] Remove the `/{order}/{delay}` redirect route +- [x] Remove the `/{order}/{delay}/{file_hash}` route +- [x] Update `root` endpoint to accept optional `order`/`delay` query params and pass them through in the redirect URL +- [x] Update `_render_page` to generate URLs with query params (`/{hash}?order=next&delay=5`) instead of path segments +- [x] Update the play/pause button URLs to use query param format + +### 3. Update `conftest.py` +- [ ] Remove `_dummy_auth_header` function and `Authorization` header from `client_dir`/`client_zip` fixtures +- [ ] Remove `set_auth_password(None)` calls from `initialized_dir`/`initialized_zip` fixtures +- [ ] Remove `password` field from `args_directory`/`args_zip` fixtures (or keep as None if still in argparse) + +### 4. Update `test_auth.py` +- [ ] Delete the entire `test_auth.py` file (all auth tests are no longer relevant) + +### 5. Update `test_endpoints.py` +- [ ] Update `TestOrderDelayRoute` tests — remove or rewrite for query param routes +- [ ] Update `TestHashPageWithRefresh` tests to use query param URLs (`/{hash}?order=next&delay=5`) +- [ ] Update `TestHashPage` tests if needed (play button URLs changed) + +### 6. Update `test_navigation.py` +- [ ] Remove `password=None` from `seeded_indexers` fixture args (if `--password` arg is removed) + +### 7. Format and verify +- [ ] Run `uv run black .` to format all code +- [ ] Run `uv run pytest` to verify all tests pass + +## Notes +- After removing auth, the `--password` CLI arg is gone entirely +- Query param format: `/{file_hash}?order=next&delay=5` +- The `order` query param accepts `"next"` or `"random"` (validated same as before) +- When `order`/`delay` are absent, behavior is identical to current browse mode diff --git a/main.py b/main.py index ce08d41..4f98325 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ -from typing import Annotated import argparse import hashlib import mimetypes @@ -7,13 +6,11 @@ import random import secrets import string import zipfile -from base64 import b64encode from glob import glob from io import BytesIO from pathlib import Path -from fastapi import FastAPI, HTTPException, Depends -from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi import FastAPI, HTTPException from fastapi.responses import ( FileResponse, HTMLResponse, @@ -25,28 +22,6 @@ app = FastAPI() file_mapping = {} indexers = [] -security = HTTPBasic() -expected_password: str | None = None - - -async def get_current_username( - credentials: Annotated[HTTPBasicCredentials, Depends(security)], -) -> str: - """Verify Basic Authentication credentials""" - if expected_password is not None and credentials.password != expected_password: - raise HTTPException( - status_code=401, - detail="Incorrect password", - headers={"WWW-Authenticate": "Basic"}, - ) - return credentials.username - - -def set_auth_password(password: str | None): - """Set the expected password for authentication""" - global expected_password - expected_password = password - class FileIndexer: def __init__(self, path: str, salt: str | None = None): @@ -158,7 +133,7 @@ async def health_check(): @app.get("/api/{file_hash}/data") -async def get_file_data(file_hash: str, username: str = Depends(get_current_username)): +async def get_file_data(file_hash: str): """Serve a specific file by its hash""" if file_hash not in file_mapping: raise HTTPException(status_code=404, detail="File not found") @@ -181,22 +156,23 @@ async def get_file_data(file_hash: str, username: str = Depends(get_current_user ) +def _build_url( + file_hash: str, order: str | None = None, delay: int | None = None +) -> str: + """Build a URL with optional order/delay query parameters.""" + base = "/{hash}".format(hash=file_hash) + if order is not None and delay is not None: + return "{base}?order={order}&delay={delay}".format( + base=base, order=order, delay=delay + ) + return base + + @app.get("/") -async def root(username: str = Depends(get_current_username)): +async def root(order: str | None = None, delay: int | None = None): """Redirect to a random file hash""" random_hash = _get_random_hash() - return RedirectResponse(url="/{hash}".format(hash=random_hash)) - - -@app.get("/{order}/{delay}") -async def order_delay( - order: str, delay: int, username: str = Depends(get_current_username) -): - """Redirect to random file with order and delay""" - random_hash = _get_random_hash() - return RedirectResponse( - url="/{order}/{delay}/{hash}".format(order=order, delay=delay, hash=random_hash) - ) + return RedirectResponse(url=_build_url(random_hash, order, delay)) def _get_navigation_data(file_hash: str, order: str | None = None): @@ -247,16 +223,16 @@ def _render_page( # Generate navigation URLs based on current mode if current_order is not None: - # Timer mode: preserve current order and delay - next_url = "/{order}/{delay}/{next_hash}".format( + # Timer mode: preserve current order and delay via query params + next_url = _build_url( + navigation_data["next_hash"], order=current_order, delay=current_delay, - next_hash=navigation_data["next_hash"], ) - prev_url = "/{order}/{delay}/{prev_hash}".format( + prev_url = _build_url( + navigation_data["prev_hash"], order=current_order, delay=current_delay, - prev_hash=navigation_data["prev_hash"], ) else: # Browse mode: generate browse mode URLs @@ -277,58 +253,58 @@ def _render_page( @app.get("/{file_hash}") -async def hash_page(file_hash: str, username: str = Depends(get_current_username)): - """Serve a page for a specific file hash with navigation""" - if file_hash not in file_mapping: - raise HTTPException(status_code=404, detail="File not found") - - navigation_data = _get_navigation_data(file_hash, order=None) - play_button = ''.format( - file_hash=file_hash - ) - return _render_page( - navigation_data, play_button=play_button, current_order=None, current_delay=None - ) - - -@app.get("/{order}/{delay}/{file_hash}") -async def hash_page_with_refresh( - order: str, - delay: int, - file_hash: str, - username: str = Depends(get_current_username), +async def hash_page( + file_hash: str, order: str | None = None, delay: int | None = None ): - """Serve a page for a specific file hash with auto-refresh navigation""" + """Serve a page for a specific file hash with optional auto-refresh navigation. + + Args: + file_hash: The hash identifier for the file. + order: Navigation order - 'next' for sequential, 'random' for random. + delay: Delay in seconds before auto-navigating to next file. + """ if file_hash not in file_mapping: raise HTTPException(status_code=404, detail="File not found") - if order not in ("next", "random"): + if order is not None and order not in ("next", "random"): raise HTTPException( status_code=400, detail="Invalid order. Must be 'next' or 'random'" ) navigation_data = _get_navigation_data(file_hash, order=order) - refresh_url = "/{order}/{delay}/{next_hash}".format( - order=order, delay=delay, next_hash=navigation_data["next_hash"] - ) + if order is not None and delay is not None: + # Timer mode: auto-refresh with query params + refresh_url = _build_url( + navigation_data["next_hash"], order=order, delay=delay + ) + refresh_meta = f'' + image_click_url = _build_url(file_hash) - refresh_meta = f'' - image_click_url = "/{file_hash}".format(file_hash=file_hash) + # Create pause button to stop auto-refresh + pause_button = ''.format( + file_hash=file_hash + ) - # Create pause button to stop auto-refresh - pause_button = ''.format( - file_hash=file_hash - ) - - return _render_page( - navigation_data, - refresh_meta, - image_click_url, - play_button=pause_button, - current_order=order, - current_delay=delay, - ) + return _render_page( + navigation_data, + refresh_meta, + image_click_url, + play_button=pause_button, + current_order=order, + current_delay=delay, + ) + else: + # Browse mode + play_button = ''.format( + file_hash=file_hash + ) + return _render_page( + navigation_data, + play_button=play_button, + current_order=None, + current_delay=None, + ) def _find_indexer_for_hash(file_hash: str): @@ -359,13 +335,9 @@ if __name__ == "__main__": parser.add_argument( "--salt", type=str, default=None, help="Salt for hashing file paths" ) - parser.add_argument( - "--password", type=str, default=None, help="Password for Basic Authentication" - ) args = parser.parse_args() initialize_server(args) - set_auth_password(args.password) import uvicorn