Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ PORT=8000
# Max upload size in megabytes (README Section 8)
MAX_UPLOAD_MB=25

# Optional shared PIN the phone must send with each request (README Section 8).
# Leave empty to disable authentication entirely.
# Optional shared PIN (README Section 8). When set, the web UI shows a
# one-time login gate that exchanges it for a session token; raw API
# requests may still send the X-API-PIN header. Empty = auth disabled.
API_PIN=

# Printer to submit jobs to (Phase 5). Empty = the Windows default printer.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Copy `.env.example` to `.env` to configure optional server settings. All keys ar
| Key | Default | Description |
|---|---|---|
| `MAX_UPLOAD_MB` | `25` | Maximum allowed upload size in megabytes (larger files rejected with HTTP 413). |
| `API_PIN` | *(empty)* | Optional security PIN. When set, requests require the `X-API-PIN` header (the web interface will display a PIN input field). |
| `API_PIN` | *(empty)* | Optional security PIN. When set, the web interface shows a one-time login gate (the PIN is exchanged for a session token, then never sent again); API clients may still send the raw `X-API-PIN` header. Leave empty to disable authentication entirely. |
| `PRINTER_NAME` | *(empty)* | Specific Windows printer name to target. If empty, the system default printer is used. |
| `SUMATRA_PATH` | *(empty)* | Custom path to `SumatraPDF.exe`. Leave empty to use automatic standard path detection. |
| `PAPER_SIZE` | *(empty)* | Default paper size token passed to driver (e.g. `A4`). Leave empty to let the Windows driver choose. |
Expand Down
53 changes: 53 additions & 0 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Auth endpoints for the PIN login gate (docs/LOGIN_PLAN.md §4).

Two endpoints, same conventions as /scanners and /print:

- GET /auth/status — the endpoint the page calls before rendering
anything. Never requires the PIN itself and never errors (always 200),
mirroring /scanners' "must be checkable before you know whether to show
UI for it" role.
- POST /auth/login — the only place the raw PIN is ever submitted.
Deliberately open (you can't require the PIN to submit the PIN); abuse
throttling is a Phase 4 add-on per LOGIN_PLAN §7/§11.
"""

import hmac

from fastapi import APIRouter, Header, HTTPException

from app.config import API_PIN
from app.models.auth import AuthStatus, LoginRequest, LoginResponse
from app.services import sessions

router = APIRouter(prefix="/auth", tags=["auth"])


@router.get("/status", response_model=AuthStatus)
def auth_status(
x_session_token: str | None = Header(default=None),
) -> AuthStatus:
"""Report whether the gate exists and whether a presented token is
still good. session_valid is None when no token was sent or when no
PIN is configured (sessions can only exist while a PIN does, so the
field is simply not meaningful then)."""
pin_required = bool(API_PIN)
session_valid = None
if pin_required and x_session_token:
session_valid = sessions.is_valid(x_session_token)
return AuthStatus(pin_required=pin_required, session_valid=session_valid)


@router.post("/login", response_model=LoginResponse)
def login(request: LoginRequest) -> LoginResponse:
"""Exchange the raw PIN for an opaque session token (one per device).

401 on a wrong PIN with a clear message; 400 when no PIN is
configured at all, since there is nothing to log in to."""
if not API_PIN:
raise HTTPException(
status_code=400,
detail="No PIN is configured — the login gate is disabled.",
)
if not request.pin or not hmac.compare_digest(request.pin, API_PIN):
raise HTTPException(status_code=401, detail="Incorrect PIN.")
return LoginResponse(token=sessions.create_session())
Loading
Loading