88197060de
Re-checked every item of the audit's own outcome section against the tree.
Most held, but five items marked fixed had residual gaps — one of them a
live IDOR — and the deferred list was untouched.
Security:
- Analytics IDOR: /api/analytics/{ws}/overview and /projection took
child_id as an unchecked query param, and get_overview called
get_days_left_at_age_4 without workspace_id, so the ownership guard
never ran. Any member could read another workspace's child projection.
Fixed at both layers.
- Scraper: session ids no longer logged at INFO; a persist-callback
failure's raw exception text and the FK maintenance banner's scraped
text no longer reach the poll client.
- Email is now case-normalised at the API boundary, with migration 009
lowercasing existing rows (aborts naming colliding rows rather than
failing opaquely on the UNIQUE constraint).
Frontend correctness:
- usePlanProposal/usePlanSync gained the stale-response guard the other
hooks already had.
- PlanSyncProvider no longer fires POST /plan/sync before auth resolves
off a blindly-read localStorage workspaceId.
- Busy flags are cleared unconditionally across all 16 useLatestRequest
consumers. setBusy(true) was unconditional while the clear was guarded
by isCurrent, so a superseded call stranded its flag forever — under
StrictMode this hung every in-app navigation behind the PageSkeleton.
isCurrent decides which response's data wins, never whether a call
releases the flag it raised.
Performance:
- Calendar no longer blanks on a mutation, and a multi-day save issues N
writes plus one background refetch instead of 2N requests.
- take_pl_on_red_days moved onto GET /auth/context, dropping a redundant
GET /workspace/:id from every cold load.
CI and repo hygiene: least-privilege permissions block, actions pinned to
commit SHAs, npm ci, uv sync --locked, failure-only artifact upload,
dependabot; dead frontend/out.js removed; CSP added to the nginx template
(repeated into the location blocks that set their own add_header, since
nginx does not merge them across levels).
Left deferred by decision: refresh-token revocation, the phase-2 contract
change, M4, M5, L2, and H12's cross-route cache. Details and the smaller
follow-ups browser verification turned up are in the audit report.
Backend 901 passed, coverage 92%. Frontend 1322 tests, coverage
98.17/94.79/92.14/98.17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PngVF38MLpdkgXcByKfdW6
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""Pydantic schemas for auth endpoints."""
|
|
|
|
from pydantic import BaseModel, EmailStr, Field, field_validator
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str = Field(min_length=8)
|
|
display_name: str = Field(min_length=1, max_length=100)
|
|
# Optional: lets a partner register straight from an invite link instead
|
|
# of first creating a throwaway solo workspace. When present, `register`
|
|
# joins the inviting workspace directly (claiming a placeholder member
|
|
# if one exists) instead of creating a new one.
|
|
invite_token: str | None = None
|
|
|
|
@field_validator("email")
|
|
@classmethod
|
|
def normalize_email(cls, v: str) -> str:
|
|
"""Lowercase at the API boundary — storage/lookup is case-sensitive
|
|
(`WHERE email = ?`), so Foo@x.se and foo@x.se must resolve to one
|
|
account rather than silently becoming two."""
|
|
return v.lower()
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
@field_validator("email")
|
|
@classmethod
|
|
def normalize_email(cls, v: str) -> str:
|
|
"""Lowercase at the API boundary — see `RegisterRequest.normalize_email`."""
|
|
return v.lower()
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
expires_in: int # seconds until expiry
|
|
|
|
|
|
class UserMeResponse(BaseModel):
|
|
id: str
|
|
email: str
|
|
display_name: str
|
|
workspace_id: str | None
|