reset endpoint + frontend reset and login pages add

This commit is contained in:
2026-09-26 19:26:57 +03:00
parent 489014667c
commit bcdf2b5154
32 changed files with 294 additions and 38 deletions
+76 -10
View File
@@ -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; `<form noValidate>`
so browser bubbles don't preempt it. Invalid field gets `.inputError`, text
goes in an absolutely positioned `.errorText` under the field. Backend
errors are logged with `console.error` and shown as a short generic message.
- **CSS Modules gotchas**: only *class* selectors are hashed. Bare element
selectors (`form {}`, `input {}`) and `#id` selectors stay global and leak
between components (one module's `form { gap }` restyled the other form).
Scope them under the module's own class (`.wrapper_1 form {}`) — the
hashed ancestor makes the rule local. Classes with hyphens are accessed as
`styles["email-icon"]`. Class names shared between two modules are
independent copies (duplicate the rules, don't rely on sharing).
- React 19 types: `FormEvent` is deprecated; use `SubmitEvent<HTMLFormElement>`.
`<p>` must not contain `<div>` (hydration warning).
- Login currently redirects to `http://localhost:8000/docs` after success
(placeholder — no post-login page yet); `ResetComponent` copies that
placeholder redirect too.
## Testing (`tests/unit`, `tests/integrated`, `tests/e2e`)
- **Recurring root cause of "different event loop" / `MissingGreenlet`-style