reset endpoint + frontend reset and login pages add
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user