--- paths: - "src/backend/**" - "src/main.rs" - "src/error.rs" - "src/config.rs" --- # Backend rules ## Server functions - One file per domain in `src/backend/`. Server-only helpers and their imports go in the `cfg_if! { if #[cfg(feature = "ssr")] { ... }}` block at the top; `#[server]` functions follow below it. Extra server-only imports inside a server fn body are written as local `use` statements. - Use plain `#[server]` for new functions (a few older ones use `#[server(name = Name, prefix = "/api", endpoint = "name")]`; both end up under `/api`). The positional form with `"Url"` no longer compiles. Server functions are registered by `leptos_routes` in `src/main.rs`; there is no separate `/api` route. - The current request is `use_context::()` (it derefs to `HttpRequest`); extractors go through `leptos_actix::extract().await?`. - Return `Result, ServerFnError>`. Expected/business failures are `Ok(ApiResponse::Error("English message".to_string()))` — the client passes that string through `trl()`, so add it to the catalogue. `Err(ServerFnError)` is for real failures (DB down etc.). - A function used with `` / `` must return `ApiResponse<()>` and take the form entity as a single struct argument; its name is the prefix of the form field names (`company: Company` ↔ `name="company[name]"`). - For ``, implement `ForValidation` for the generated struct (PascalCase of the fn name) right below the function: ```rust impl ForValidation for UpdateCompany { fn entity(&self) -> &dyn Validate { &self.company } } ``` That only validates in the browser. Anything that matters must be checked again inside the server fn. ## Authorization - Every server fn that reads or changes non-public data starts with `perm_check!(is_logged_in)` or `perm_check!(is_admin)`; use `user_check!(login)` when a non-admin may only touch their own record. The macros return early with 403 and `ApiResponse::Error`, so they only work in functions returning `Result, _>`. - The auth middleware protects only `/admin*` page requests. `/api/*` is not covered by it — a server fn without `perm_check!` is public, whether or not any page calls it. Only the endpoints of the public booking form and of the customer's booking page should be. - A server fn that does not return `ApiResponse` can't use the macros; it starts with `require_login().await?` (`src/backend/user.rs`). - The test `every_server_fn_checks_permissions_or_is_listed_as_public` in `src/backend/mod.rs` scans the sources and fails for a server fn that neither checks the user nor is on its `PUBLIC_SERVER_FNS` list. Put a function on the list only when it returns nothing about other people — in particular never a booking `uuid`, which is the secret of the customer's booking page. - Server-side code must not call a permission-checked server fn to get data for a request of a customer (e.g. mail templates when a booking is made); use the helper underneath (`message_for_type`). - Plain Actix handlers must check the session themselves. - Files are uploaded through a multipart server function (`upload_image` in `src/backend/appearance.rs`): `#[server(input = MultipartFormData)]`, called in the browser through `Action::new_local` with a `web_sys::FormData`. The server stores a file under a name it makes itself, checks size and content, and never trusts the name sent by the browser. The multipart codec panics on a request without a boundary; `is_broken_upload` in `src/main.rs` refuses those first — extend it when adding another multipart function. - HTML that comes from a user and is rendered with `inner_html` (the text under the banner) must go through `sanitize_text` in `src/backend/rich_text.rs` before it is stored. Extend its allow-list together with the editor toolbar, never the other way round. - The logged-in `User` is stored in the cookie session under `"user"`; read it with `logged_in_user()`. ## Database access - Get the pool with `get_pool().await?` (and the mailer with `get_mailing().await?`); they come from `AppData` in Actix app data. - Runtime-checked sqlx only: `query` / `query_as::<_, T>` with `$n` binds. No `query!` macros (there is no `DATABASE_URL`/offline data at build time). - Always bind values; never format user input into SQL. - Multi-statement writes go through a transaction (`pool.begin()`, helpers take `&mut Transaction<'_, Postgres>`), as in `create_reservation`. - The users table is `"user"` — always quoted. ## Data types (`data.rs`) - Types crossing the server/client boundary live in `src/backend/data.rs` and must compile for WASM: sqlx derives only as `#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]` / `sqlx::Type`. - `id` fields are private with an `id()` getter; other fields are mostly `pub`. - Validation uses `validator` 0.16 derive attributes; every `message = "..."` is an English translation key. - Rust enums mapped to Postgres enums (`SlotType`, `ReservationState`, `MessageType`) must keep variant names identical to the SQL enum labels. ## Errors, logging, mail - Server-side helpers return `AppError`; add a `From` impl in `src/error.rs` for new error sources (ssr-only ones behind `#[cfg(feature = "ssr")]`). The `From` impls already log the error. - Log with the `log` macros (`info!`, `warn!`, `error!`), not `println!`. - Send mail through `Mailer::send_mail` (queued to a background sender thread), never directly from a request. Templates are `message` rows keyed by `MessageType` with `#date#`, `#summary#` and `#link#` placeholders; the link to the booking page is added with `MailMessage::with_link`, built from `get_base_url()`. - `get_booking` and `cancel_booking` are public on purpose: the uuid of a booking is the secret. They must never return or log more about the customer than the e-mails contain, and must answer an unknown uuid the same way as a malformed one. Anything that changes a booking stays a POST behind a button — mail clients open links on their own. - Times of bookings are naive local times; compare them with `Local::now().naive_local()`. - A new `[section]`/key in `config.toml` needs the struct in `src/config.rs` and an entry in `config_sample.toml`.