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