Files
rezervovator/.claude/rules/backend.md
T

4.6 KiB

paths
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::<leptos_actix::Request>() (it derefs to HttpRequest); extractors go through leptos_actix::extract().await?.

  • Return Result<ApiResponse<T>, 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 <DataForm> / <QuestionDialog> 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 <DataForm>, implement ForValidation for the generated struct (PascalCase of the fn name) right below the function:

    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<ApiResponse<_>, _>.
  • The auth middleware protects only /admin* page requests. /api/* is not covered by it — a server fn without perm_check! is public. Only the public booking form endpoints should be.
  • 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.
  • 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# and #summary# placeholders.
  • A new [section]/key in config.toml needs the struct in src/config.rs and an entry in config_sample.toml.