# 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=<wanted-path>`.
- 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=<path>`.
- 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
```
