# Local family dashboard admin auth pattern

Use for local family/PWA dashboards where admin-only screens must be protected without building a full user/role system.

## Scope

- Keep this as a small PIN-protected admin gate, not a general identity system.
- Store the admin PIN only as a salted PBKDF2 hash (`pbkdf2_sha256$iterations$salt$digest`) in `AdminCredential`.
- Create short-lived `AdminSession` rows with a random bearer-style token; store only `sha256(token)` in SQLite.
- Return the raw token only once at login. Frontend may keep it in browser storage for the local iPad app and send it as `X-Admin-Session`.

## API pattern

- `POST /api/admin/auth/login` with `{pin}` returns `{token, expires_at, username}`.
- `GET /api/admin/auth/me` validates the token and returns current admin/session metadata.
- `POST /api/admin/auth/logout` revokes the session.
- Protect all `/api/admin/*` mutation/read routes with the token dependency, except `/api/admin/auth/login`.
- Child-facing actions such as task completion or benefit redeem can remain public/local-touch actions when the blueprint expects child operation.

## SQLite datetime pitfall

SQLite/SQLModel can round-trip timezone-aware datetimes as offset-naive values. Avoid comparing aware `datetime.now(timezone.utc)` to DB-loaded naive `expires_at`. Use a consistent representation, e.g. UTC naive internally for session expiry:

```python
def now_utc_naive() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)
```

Serialize with `.isoformat()` for API responses.

## Frontend pattern

- Admin panel should show only a PIN form when no token is present.
- After login, load protected admin resources with `X-Admin-Session`.
- Invalidate protected queries after login/logout and after admin mutations.
- On logout or invalid session, remove the token locally and return to the locked state.
- Keep large touch targets for iPad; do not hide admin state behind small desktop-only controls.

## Tests / smoke

Minimum backend tests:

- unauthenticated `/api/admin/...` returns `401`;
- wrong PIN returns `401`;
- correct PIN opens `/api/admin/auth/me`;
- logout revokes the token;
- stored PIN hash is not the cleartext default and has the expected hash prefix.

Minimum smoke:

```text
blocked 401
wrong_pin 401
login 200 True
me 200 True
admin_resource 200
logout 200
after_logout 401
```
