diff --git a/CLAUDE.md b/CLAUDE.md
index d31f5be..5b80f96 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -20,6 +20,13 @@ workers. Python >=3.13,<4.0, Poetry for dependency management.
- `secure` flag on refresh_token cookie is driven by `env_settings.PROD_MODE`
(bool) — `False` locally/tests so cookies work over plain HTTP, `True` in
prod.
+- **CORS**: `CORSMiddleware` in `main.py` with `allow_origins=[env_settings.
+ FRONT_URL]` and `allow_credentials=True` (needed for the httpOnly
+ `refresh_token` cookie — with credentials, `["*"]` is rejected by browsers).
+ `FRONT_URL` is a required `Env` field with no default, so the backend fails
+ to start without it in `configs/.env` (e.g. `http://localhost:5173`). It is
+ added last, after the logging middlewares, so it wraps them and CORS
+ headers also appear on error responses (401/429).
## Redis (`src/cache/`)
@@ -27,12 +34,15 @@ workers. Python >=3.13,<4.0, Poetry for dependency management.
`redis.Redis` directly (inherits all commands, no manual wrapping needed).
- Three uses: permissions is-cache was considered and rejected (no real DB
savings — `get_user_by_id` already eager-loads everything via `selectin`
- in one call); rate limiting on login (`RateLimit.rate_limit(ip)` —
- `INCR` + `EXPIRE` on first attempt, blocks >5/60s); access-token revoke
- blacklist (see above).
-- Rate limit is only triggered inside `except HTTPException` on `/protected/token`
- — i.e. only on failed logins, not successful ones (otherwise legitimate
- repeated logins would trip it).
+ in one call); rate limiting (`RateLimit.rate_limit(ip, action, limit=5,
+ window=60)` — `INCR` + `EXPIRE ... NX` on first attempt, key
+ `rate_limit:{action}:{ip}`, so each action has its own counter); access-token
+ revoke blacklist (see above).
+- Two rate-limited actions: `"login"` (5/60s) and `"reset"` (3/600s).
+ The login limit is only triggered inside `except HTTPException` on
+ `/protected/token` — i.e. only on failed logins, not successful ones
+ (otherwise legitimate repeated logins would trip it). `/protected/reset`
+ counts every call.
## RabbitMQ (`src/messaging/`)
@@ -96,10 +106,19 @@ workers. Python >=3.13,<4.0, Poetry for dependency management.
reads both `email` and `temp_password` from the message body and uses the
same transient/permanent classification as `WelcomeEmailConsumer`.
`EmailProducer.send_reset_email(email, temp_password)` takes the password
- as a second argument now. **Still missing**: nothing in the app actually
- calls `send_reset_email` yet — there's no password-reset route that
- generates a `temp_password` and publishes it. Don't assume the
- reset-password feature is reachable end-to-end until that route exists.
+ as a second argument.
+- **Reset route**: `POST /protected/reset?email=...` in `auth_routes.py` —
+ `email` is a plain `str` param, so FastAPI reads it from the **query
+ string**, not the JSON body. The route rate-limits (3/600s per IP), calls
+ `CrudService.reset_password(email)` (generates a 16-char temp password via
+ `HashService.generate_temp_password()` — which is `async` — hashes and
+ saves it through `update_user`), then publishes `{email, temp_password}` to
+ `email.reset`. A 404 (unknown email) is swallowed so the response is always
+ `{"detail": "New password was sent"}` — no user enumeration. Known rough
+ edge: the DB password is changed *before* the email is sent, so a message
+ that ends up in the DLQ leaves the user with a changed password and no
+ email; and anyone who knows an email can reset that account's password.
+ A token-link flow would avoid both.
## Logging (`src/logging/`)
@@ -218,6 +237,53 @@ workers. Python >=3.13,<4.0, Poetry for dependency management.
--force` if it persists across rebuilds; a one-off warning on first
build isn't necessarily a problem.
+## Frontend (`frontend/`)
+
+- React 19 + TypeScript + Vite + `react-router-dom` 7, package manager is
+ **yarn** (`yarn.lock`). Run with `yarn dev` (Vite, default
+ `http://localhost:5173`). The user writes/edits frontend code themselves —
+ explain rather than edit unless asked (see memory).
+- Layout: `src/features/auth/components/` (`AuthComponent`, `ResetComponent`,
+ each with its own `.module.css`), `src/pages/` (`LoginPage` at `/`, `ForgotPasswordPage`
+ at `/forgot-password` — thin wrappers), `src/layouts/AuthLayout` (shared
+ split-screen shell with the marketing block; takes `children` for the form
+ on the right), `src/api/` (`token.ts`, `LoginAPI/Auth.ts`,
+ `ResetAPI/Reset.ts`), `src/types/UserTypes.ts` (mirrors the Pydantic
+ `UserOut`/`UserCreate`). `index.css` = global reset/font, `App.css` = `#root`
+ frame.
+- **Env**: Vite reads `.env*` from `frontend/configs/` (`envDir: "./configs"`
+ in `vite.config.ts`), not from `frontend/`. Only `VITE_`-prefixed variables
+ reach the browser code, via `import.meta.env`. Backend address is
+ `VITE_BACKEND_URL`, exported as `API_URL` from `src/api/token.ts`. Restart
+ the dev server after editing `.env`. There is **no** Vite `server.proxy`
+ (removed on purpose) — the browser calls the backend directly, so backend
+ CORS (see Auth) must allow the frontend origin.
+- **Access token** lives in a module variable in `src/api/token.ts`
+ (`setAccessToken`/`getAccessToken`), not in `localStorage`; it is lost on
+ page reload (a `/protected/refresh` call on startup is not implemented
+ yet). `authFetch(path, init)` adds `Authorization: Bearer` and
+ `credentials: "include"`. Login (`POST /protected/token`) must be sent as
+ **form-data** (`URLSearchParams` with `username` = email and `password`),
+ because the route uses `OAuth2PasswordRequestForm`, and returns
+ `{access_token, token_type}`; the refresh token arrives as an httpOnly cookie.
+- Forms are controlled inputs (`useState`) with hand-written validation:
+ per-field `errors` state, checked on blur and on submit; `