# l4d2 Web Auth Pages Design Date: 2026-05-06 ## Goal Replace the current plain-text login/signup placeholders with a small, functional authentication UI for the Flask web app. Keep the current username/password schema for now, remove public signup, and redirect users back to the page they originally wanted after login when that target is safe. ## Scope In scope: - Real server-rendered `/login` page using the existing neutral shell and CSS tokens. - Username/password login using the existing `users.username` and `users.password_digest` fields. - Removal of `/signup` handlers, links, and CSRF exemptions. - Redirect anonymous protected requests to `/login?next=`. - Redirect successful logins to a safe local `next` target, otherwise `/dashboard`. - Redirect `/` to `/login` for anonymous users and `/dashboard` for logged-in users. - Keep `/health` public for monitoring. Out of scope: - Email-based login. - Steam login. - Account self-registration. - Password reset or password change flows. - User invitation flows. - Schema changes to split users, credentials, and external identities. ## Routing Behavior `GET /login` renders the login page. The page accepts an optional `next` query parameter and preserves it in a hidden form field so a successful POST can return the user to the requested page. `POST /login` validates the username and password with the existing password hash helpers. On success, it stores `session["user_id"]` and redirects to the safe local `next` value if present. If `next` is missing, empty, or unsafe, it redirects to `/dashboard`. `GET /signup` and `POST /signup` are removed. They should return Flask's normal `404 Not Found` response. `GET /` redirects to `/login` for anonymous users and `/dashboard` for logged-in users. Protected page decorators keep their current authorization semantics, but anonymous redirects include the requested path in `next`. Logged-in non-admin users still receive `403 Forbidden` for admin-only pages. ## Safe Redirect Rules A `next` target is safe only when it is a local absolute path: - It must start with `/`. - It must not start with `//`. - It must not contain a URL scheme such as `https://`. - It must not contain backslashes, which browsers may normalize into path separators. - The same checks apply after one percent-decoding pass so encoded backslashes and encoded protocol-relative paths are rejected. Unsafe values are ignored and replaced with `/dashboard`. This avoids open redirects while keeping the implementation simple. ## UI Behavior The login page should be minimal and consistent with the current admin-console UI: - Brand/title: `left4me`. - Short explanatory text. - Fields: `username` and `password`. - Submit button: `log in`. - No signup link. The shared shell should not show main application navigation to anonymous users. Anonymous users may see the brand only. Logged-in users keep the existing navigation and account controls. ## Data Model No schema change is planned for this iteration. `User.username` remains the local login identifier. This intentionally defers email-based and Steam-based identity modeling until those flows are designed. Future Steam login should use a separate identity model rather than forcing Steam users into an email/password shape, but that is not part of this change. ## Error Handling Invalid credentials keep the current simple behavior: `401 invalid credentials`. Missing username or password also remain simple response errors. The first UI pass does not need inline validation messages or flashed form errors. Rate limiting on `POST /login` remains unchanged. ## Testing Tests should cover: - `GET /login` renders a form with username and password inputs. - `GET /signup` returns `404`. - `POST /signup` returns `404` rather than being intercepted by CSRF handling. - Anonymous protected pages redirect to `/login?next=`. - A successful login with a safe `next` redirects to that target. - A successful login with an unsafe `next` redirects to `/dashboard`. - A successful login with a backslash-containing `next` redirects to `/dashboard`. - A successful login with a percent-encoded backslash in `next` redirects to `/dashboard`. - Anonymous `/` redirects to `/login`. - Logged-in `/` redirects to `/dashboard`. - Non-admin users still receive `403` for admin pages. ## Verification Run these checks after implementation: ```bash pytest l4d2web/tests/test_auth.py -q pytest l4d2web/tests/test_pages.py -q pytest l4d2web/tests/test_security.py -q pytest l4d2web/tests -q ```