From 1bd3e17672cdf5428bc17a779cea7159fb276cad Mon Sep 17 00:00:00 2001 From: AB Date: Sat, 11 Jul 2026 14:11:31 +0300 Subject: [PATCH] Moved to PSQL. --- AGENTS.md | 60 ++ CLAUDE.md | 46 +- Cargo.lock | 3 +- Cargo.toml | 4 +- src/main.rs | 16 +- src/migrations.rs | 16 +- src/migrations/m_0001_initial.rs | 835 +++++++++----------- src/migrations/m_0002_visit_schedule.rs | 90 --- src/migrations/m_0003_visit_feedback.rs | 24 - src/migrations/m_0004_visit_public_notes.rs | 24 - src/migrations/m_0005_testimonials.rs | 56 -- src/migrations/m_0006_user_telegram.rs | 35 - src/public.rs | 22 +- 13 files changed, 483 insertions(+), 748 deletions(-) create mode 100644 AGENTS.md delete mode 100644 src/migrations/m_0002_visit_schedule.rs delete mode 100644 src/migrations/m_0003_visit_feedback.rs delete mode 100644 src/migrations/m_0004_visit_public_notes.rs delete mode 100644 src/migrations/m_0005_testimonials.rs delete mode 100644 src/migrations/m_0006_user_telegram.rs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1ba013a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## Project Overview + +Pet sitting web service for managing clients and bookings. The owner uses the site to: +- Receive and manage client requests (leads) from the website +- Schedule calls and visits with clients +- Upload photos/videos of pets for remote viewing by clients (public media page via unique token) +- Get Telegram notifications about new requests + +## Tech Stack + +- **Language:** Rust (edition 2024) +- **Web framework:** [Cot](https://github.com/cot-rs/cot) - Rust web framework (Django-like), local path `../cot/cot` +- **Database:** PostgreSQL (via Cot ORM) +- **Notifications:** Telegram Bot API + +## Build & Run + +```sh +cargo build # build +cargo run # run dev server at http://127.0.0.1:8000 +cargo test # run all tests +cargo test # run a single test by name +cargo clippy # lint +cargo fmt --check # check formatting +cot migration make # generate migrations from model changes (requires cot-cli) +``` + +Set `WEB_PETTING_DATABASE_URL` (or `DATABASE_URL`) before running the app or migrations. Example: + +```sh +WEB_PETTING_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/web_petting cargo run +``` + +## Architecture + +Monolithic Cot web app with a single PostgreSQL database. + +- `src/main.rs` - project/app setup, router, config +- `src/models.rs` - all database models (Lead, Client, Visit, Media, User, Setting) +- `src/migrations.rs` - migration registry +- `src/migrations/` - migration files + +## Database Design Principles + +- **Soft-delete everywhere:** records are never physically deleted, only status changes (e.g. `active` -> `archived`, `new` -> `rejected`). This ensures data can always be recovered. +- **Status fields** are stored as `String` with enum-like values defined in `models.rs`. +- **Foreign keys** use `cot::db::ForeignKey` with `Restrict` on delete/update. + +## Data Model + +- **Lead** (`new`/`in_progress`/`converted`/`rejected`) - public form submission; links to Client when converted +- **Client** (`active`/`archived`) - confirmed client with `media_token` for public media page +- **Visit** (`scheduled`/`completed`/`cancelled`/`deleted`) - pet sitting session, belongs to Client and User +- **Media** (`active`/`archived`) - photo/video, belongs to Client, optionally to Visit +- **User** (`active`/`archived`) - admin accounts (supports multiple admins) +- **Setting** - global key-value config (telegram_bot_token, telegram_chat_id, etc.) diff --git a/CLAUDE.md b/CLAUDE.md index 37d9c0e..3f7f598 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,30 +13,36 @@ Pet sitting web service for managing clients and bookings. The owner uses the si ## Tech Stack - **Language:** Rust (edition 2024) -- **Web framework:** [Cot](https://github.com/cot-rs/cot) — Rust web framework (Django-like), local path `../cot/cot` -- **Database:** SQLite (via Cot ORM), file `db.sqlite3` +- **Web framework:** [Cot](https://github.com/cot-rs/cot) - Rust web framework (Django-like), local path `../cot/cot` +- **Database:** PostgreSQL (via Cot ORM) - **Notifications:** Telegram Bot API ## Build & Run ```sh -cargo build # build -cargo run # run dev server at http://127.0.0.1:8000 -cargo test # run all tests -cargo test # run a single test by name -cargo clippy # lint -cargo fmt --check # check formatting -cot migration make # generate migrations from model changes (requires cot-cli) +cargo build # build +cargo run # run dev server at http://127.0.0.1:8000 +cargo test # run all tests +cargo test # run a single test by name +cargo clippy # lint +cargo fmt --check # check formatting +cot migration make # generate migrations from model changes (requires cot-cli) +``` + +Set `WEB_PETTING_DATABASE_URL` (or `DATABASE_URL`) before running the app or migrations. Example: + +```sh +WEB_PETTING_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/web_petting cargo run ``` ## Architecture -Monolithic Cot web app with a single SQLite database. +Monolithic Cot web app with a single PostgreSQL database. -- `src/main.rs` — project/app setup, router, config -- `src/models.rs` — all database models (Lead, Client, Visit, Media, User, Setting) -- `src/migrations.rs` — migration registry (auto-generated by `cot migration make`) -- `src/migrations/` — migration files (auto-generated) +- `src/main.rs` - project/app setup, router, config +- `src/models.rs` - all database models (Lead, Client, Visit, Media, User, Setting) +- `src/migrations.rs` - migration registry +- `src/migrations/` - migration files ## Database Design Principles @@ -46,9 +52,9 @@ Monolithic Cot web app with a single SQLite database. ## Data Model -- **Lead** (`new`/`in_progress`/`converted`/`rejected`) — public form submission; links to Client when converted -- **Client** (`active`/`archived`) — confirmed client with `media_token` for public media page -- **Visit** (`scheduled`/`completed`/`cancelled`) — pet sitting session, belongs to Client -- **Media** (`active`/`archived`) — photo/video, belongs to Client, optionally to Visit -- **User** (`active`/`archived`) — admin accounts (supports multiple admins) -- **Setting** — global key-value config (telegram_bot_token, telegram_chat_id, etc.) +- **Lead** (`new`/`in_progress`/`converted`/`rejected`) - public form submission; links to Client when converted +- **Client** (`active`/`archived`) - confirmed client with `media_token` for public media page +- **Visit** (`scheduled`/`completed`/`cancelled`/`deleted`) - pet sitting session, belongs to Client and User +- **Media** (`active`/`archived`) - photo/video, belongs to Client, optionally to Visit +- **User** (`active`/`archived`) - admin accounts (supports multiple admins) +- **Setting** - global key-value config (telegram_bot_token, telegram_chat_id, etc.) diff --git a/Cargo.lock b/Cargo.lock index 90c3740..659d536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,7 +1617,6 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -3467,7 +3466,7 @@ dependencies = [ [[package]] name = "web-petting" -version = "0.1.14" +version = "0.1.15" dependencies = [ "base64", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 38d1300..f327a6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "web-petting" -version = "0.1.15" +version = "1.0.0" edition = "2024" [dependencies] -cot = { version = "0.6.0", features = ["sqlite"] } +cot = { version = "0.6.0", default-features = false, features = ["postgres", "json"] } chrono = "0.4" chrono-tz = "0.10" serde = { version = "1", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index 0e066b6..e03ea27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,12 @@ fn debug_enabled(config_name: &str) -> bool { }) } +fn database_url() -> String { + std::env::var("WEB_PETTING_DATABASE_URL") + .or_else(|_| std::env::var("DATABASE_URL")) + .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5432/web_petting".to_string()) +} + impl Project for PettingProject { fn cli_metadata(&self) -> CliMetadata { cot::cli::metadata!() @@ -77,11 +83,7 @@ impl Project for PettingProject { fn config(&self, config_name: &str) -> cot::Result { Ok(ProjectConfig::builder() .debug(debug_enabled(config_name)) - .database( - DatabaseConfig::builder() - .url("sqlite://db.sqlite3?mode=rwc") - .build(), - ) + .database(DatabaseConfig::builder().url(database_url()).build()) .middlewares( MiddlewareConfig::builder() .session( @@ -121,8 +123,6 @@ impl Project for PettingProject { fn main() -> impl Project { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); - let _ = tracing_subscriber::fmt() - .with_env_filter(filter) - .try_init(); + let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init(); PettingProject } diff --git a/src/migrations.rs b/src/migrations.rs index e62bf6a..b0ab240 100644 --- a/src/migrations.rs +++ b/src/migrations.rs @@ -1,19 +1,7 @@ //! List of migrations for the current app. //! -//! Generated by cot CLI 0.6.0 on 2026-04-29 10:36:47+00:00 +//! Squashed for the PostgreSQL migration on 2026-07-11. pub mod m_0001_initial; -pub mod m_0002_visit_schedule; -pub mod m_0003_visit_feedback; -pub mod m_0004_visit_public_notes; -pub mod m_0005_testimonials; -pub mod m_0006_user_telegram; /// The list of migrations for current app. -pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[ - &m_0001_initial::Migration, - &m_0002_visit_schedule::Migration, - &m_0003_visit_feedback::Migration, - &m_0004_visit_public_notes::Migration, - &m_0005_testimonials::Migration, - &m_0006_user_telegram::Migration, -]; +pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[&m_0001_initial::Migration]; diff --git a/src/migrations/m_0001_initial.rs b/src/migrations/m_0001_initial.rs index 8eefeff..ad60e5c 100644 --- a/src/migrations/m_0001_initial.rs +++ b/src/migrations/m_0001_initial.rs @@ -1,7 +1,8 @@ -//! Generated by cot CLI 0.6.0 on 2026-04-29 10:36:47+00:00 +//! Initial PostgreSQL schema for the current data model. #[derive(Debug, Copy, Clone)] pub(super) struct Migration; + impl ::cot::db::migrations::Migration for Migration { const APP_NAME: &'static str = "web-petting"; const MIGRATION_NAME: &'static str = "m_0001_initial"; @@ -9,479 +10,395 @@ impl ::cot::db::migrations::Migration for Migration { const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[ ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__user")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("login"), - ::TYPE, - ) - .set_null(::NULLABLE) - .unique(), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("password_hash"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("display_name"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("updated_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("login"), + ::TYPE, + ) + .set_null(::NULLABLE) + .unique(), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("password_hash"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("display_name"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("telegram_chat_id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("telegram_notifications"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("updated_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__setting")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("key"), - ::TYPE, - ) - .set_null(::NULLABLE) - .unique(), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("value"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("updated_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("key"), + ::TYPE, + ) + .set_null(::NULLABLE) + .unique(), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("value"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("updated_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__client")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("name"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("phone"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("email"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("address"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("notes"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("media_token"), - ::TYPE, - ) - .set_null(::NULLABLE) - .unique(), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("updated_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("name"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("phone"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("email"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("address"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("notes"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("media_token"), + ::TYPE, + ) + .set_null(::NULLABLE) + .unique(), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("color"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("updated_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) + .build(), + ::cot::db::migrations::Operation::create_model() + .table_name(::cot::db::Identifier::new("web_petting__testimonial")) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("text"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("author_note"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("image_path"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("sort_order"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__visit")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("client_id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .foreign_key( - ::TABLE_NAME, - ::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("scheduled_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("duration_minutes"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("notes"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("updated_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("client_id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::ForeignKeyOnDeletePolicy::Restrict, + ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + ) + .set_null( + as ::cot::db::DatabaseField>::NULLABLE, + ), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("user_id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::ForeignKeyOnDeletePolicy::Restrict, + ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + ) + .set_null( + as ::cot::db::DatabaseField>::NULLABLE, + ), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("visit_date"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("time_start"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("time_end"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("notes"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("public_notes"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("client_feedback"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("updated_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__media")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("client_id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .foreign_key( - ::TABLE_NAME, - ::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("visit_id"), - , - > as ::cot::db::DatabaseField>::TYPE, - ) - .foreign_key( - ::TABLE_NAME, - ::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, - ) - .set_null( - , - > as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("file_path"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("file_type"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("caption"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("client_id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::ForeignKeyOnDeletePolicy::Restrict, + ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + ) + .set_null( + as ::cot::db::DatabaseField>::NULLABLE, + ), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("visit_id"), + > as ::cot::db::DatabaseField>::TYPE, + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::ForeignKeyOnDeletePolicy::Restrict, + ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + ) + .set_null( + > as ::cot::db::DatabaseField>::NULLABLE, + ), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("file_path"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("file_type"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("caption"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ::cot::db::migrations::Operation::create_model() .table_name(::cot::db::Identifier::new("web_petting__lead")) - .fields( - &[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("name"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("phone"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("email"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("comment"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( - as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("client_id"), - , - > as ::cot::db::DatabaseField>::TYPE, - ) - .foreign_key( - ::TABLE_NAME, - ::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, - ) - .set_null( - , - > as ::cot::db::DatabaseField>::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("updated_at"), - ::TYPE, - ) - .set_null( - ::NULLABLE, - ), - ], - ) + .fields(&[ + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .auto() + .primary_key() + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("name"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("phone"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("email"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("comment"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("status"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("client_id"), + > as ::cot::db::DatabaseField>::TYPE, + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::ForeignKeyOnDeletePolicy::Restrict, + ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + ) + .set_null( + > as ::cot::db::DatabaseField>::NULLABLE, + ), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("created_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("updated_at"), + ::TYPE, + ) + .set_null(::NULLABLE), + ]) .build(), ]; } - -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _Client { - #[model(primary_key)] - pub id: cot::db::Auto, - pub name: String, - pub phone: Option, - pub email: Option, - pub address: Option, - pub notes: Option, - /// Unique token for the public media page (client views photos/videos here). - #[model(unique)] - pub media_token: String, - /// active | archived - pub status: String, - pub created_at: chrono::NaiveDateTime, - pub updated_at: chrono::NaiveDateTime, -} -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _Lead { - #[model(primary_key)] - pub id: cot::db::Auto, - pub name: String, - pub phone: Option, - pub email: Option, - pub comment: Option, - /// new | in_progress | converted | rejected - pub status: String, - pub client_id: Option>, - pub created_at: chrono::NaiveDateTime, - pub updated_at: chrono::NaiveDateTime, -} -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _Media { - #[model(primary_key)] - pub id: cot::db::Auto, - pub client_id: cot::db::ForeignKey, - pub visit_id: Option>, - pub file_path: String, - /// photo | video - pub file_type: String, - pub caption: Option, - /// active | archived - pub status: String, - pub created_at: chrono::NaiveDateTime, -} -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _Setting { - #[model(primary_key)] - pub id: cot::db::Auto, - #[model(unique)] - pub key: String, - pub value: String, - pub updated_at: chrono::NaiveDateTime, -} -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _User { - #[model(primary_key)] - pub id: cot::db::Auto, - #[model(unique)] - pub login: String, - pub password_hash: String, - pub display_name: Option, - /// active | archived - pub status: String, - pub created_at: chrono::NaiveDateTime, - pub updated_at: chrono::NaiveDateTime, -} -#[derive(::core::fmt::Debug)] -#[::cot::db::model(model_type = "migration")] -struct _Visit { - #[model(primary_key)] - pub id: cot::db::Auto, - pub client_id: cot::db::ForeignKey, - pub scheduled_at: chrono::NaiveDateTime, - pub duration_minutes: Option, - pub notes: Option, - /// scheduled | completed | cancelled - pub status: String, - pub created_at: chrono::NaiveDateTime, - pub updated_at: chrono::NaiveDateTime, -} diff --git a/src/migrations/m_0002_visit_schedule.rs b/src/migrations/m_0002_visit_schedule.rs deleted file mode 100644 index 7742732..0000000 --- a/src/migrations/m_0002_visit_schedule.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Migration: update Visit model for scheduling + add Client.color -//! Visit: Remove scheduled_at, duration_minutes; Add user_id, visit_date, time_start, time_end -//! Client: Add color - -#[derive(Debug, Copy, Clone)] -pub(super) struct Migration; -impl ::cot::db::migrations::Migration for Migration { - const APP_NAME: &'static str = "web-petting"; - const MIGRATION_NAME: &'static str = "m_0002_visit_schedule"; - const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = - &[::cot::db::migrations::MigrationDependency::migration( - "web-petting", - "m_0001_initial", - )]; - const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[ - // Add color to client (nullable for existing rows) - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__client")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("color"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE) - ) - .build(), - // Remove old visit fields - ::cot::db::migrations::Operation::remove_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field(::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("scheduled_at"), - ::TYPE, - )) - .build(), - ::cot::db::migrations::Operation::remove_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field(::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("duration_minutes"), - as ::cot::db::DatabaseField>::TYPE, - ).set_null( as ::cot::db::DatabaseField>::NULLABLE)) - .build(), - // Add new fields - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("user_id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .foreign_key( - ::TABLE_NAME, - ::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE) - ) - .build(), - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("visit_date"), - ::TYPE, - ) - .set_null(::NULLABLE) - ) - .build(), - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("time_start"), - ::TYPE, - ) - .set_null(::NULLABLE) - ) - .build(), - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("time_end"), - ::TYPE, - ) - .set_null(::NULLABLE) - ) - .build(), - ]; -} diff --git a/src/migrations/m_0003_visit_feedback.rs b/src/migrations/m_0003_visit_feedback.rs deleted file mode 100644 index fae36ef..0000000 --- a/src/migrations/m_0003_visit_feedback.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Migration: add client_feedback to Visit - -#[derive(Debug, Copy, Clone)] -pub(super) struct Migration; -impl ::cot::db::migrations::Migration for Migration { - const APP_NAME: &'static str = "web-petting"; - const MIGRATION_NAME: &'static str = "m_0003_visit_feedback"; - const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = - &[::cot::db::migrations::MigrationDependency::migration( - "web-petting", - "m_0002_visit_schedule", - )]; - const OPERATIONS: &'static [::cot::db::migrations::Operation] = - &[::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("client_feedback"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ) - .build()]; -} diff --git a/src/migrations/m_0004_visit_public_notes.rs b/src/migrations/m_0004_visit_public_notes.rs deleted file mode 100644 index f957632..0000000 --- a/src/migrations/m_0004_visit_public_notes.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Migration: add public_notes to Visit - -#[derive(Debug, Copy, Clone)] -pub(super) struct Migration; -impl ::cot::db::migrations::Migration for Migration { - const APP_NAME: &'static str = "web-petting"; - const MIGRATION_NAME: &'static str = "m_0004_visit_public_notes"; - const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = - &[::cot::db::migrations::MigrationDependency::migration( - "web-petting", - "m_0003_visit_feedback", - )]; - const OPERATIONS: &'static [::cot::db::migrations::Operation] = - &[::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__visit")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("public_notes"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ) - .build()]; -} diff --git a/src/migrations/m_0005_testimonials.rs b/src/migrations/m_0005_testimonials.rs deleted file mode 100644 index 424429d..0000000 --- a/src/migrations/m_0005_testimonials.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Migration: create Testimonial table - -#[derive(Debug, Copy, Clone)] -pub(super) struct Migration; -impl ::cot::db::migrations::Migration for Migration { - const APP_NAME: &'static str = "web-petting"; - const MIGRATION_NAME: &'static str = "m_0005_testimonials"; - const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = - &[::cot::db::migrations::MigrationDependency::migration( - "web-petting", - "m_0004_visit_public_notes", - )]; - const OPERATIONS: &'static [::cot::db::migrations::Operation] = - &[::cot::db::migrations::Operation::create_model() - .table_name(::cot::db::Identifier::new("web_petting__testimonial")) - .fields(&[ - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .auto() - .primary_key() - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("text"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("author_note"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("image_path"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("status"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("sort_order"), - ::TYPE, - ) - .set_null(::NULLABLE), - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("created_at"), - ::TYPE, - ) - .set_null(::NULLABLE), - ]) - .build()]; -} diff --git a/src/migrations/m_0006_user_telegram.rs b/src/migrations/m_0006_user_telegram.rs deleted file mode 100644 index 53951c0..0000000 --- a/src/migrations/m_0006_user_telegram.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Migration: add telegram_chat_id and telegram_notifications to User - -#[derive(Debug, Copy, Clone)] -pub(super) struct Migration; -impl ::cot::db::migrations::Migration for Migration { - const APP_NAME: &'static str = "web-petting"; - const MIGRATION_NAME: &'static str = "m_0006_user_telegram"; - const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = - &[::cot::db::migrations::MigrationDependency::migration( - "web-petting", - "m_0005_testimonials", - )]; - const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[ - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__user")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("telegram_chat_id"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ) - .build(), - ::cot::db::migrations::Operation::add_field() - .table_name(::cot::db::Identifier::new("web_petting__user")) - .field( - ::cot::db::migrations::Field::new( - ::cot::db::Identifier::new("telegram_notifications"), - as ::cot::db::DatabaseField>::TYPE, - ) - .set_null( as ::cot::db::DatabaseField>::NULLABLE), - ) - .build(), - ]; -} diff --git a/src/public.rs b/src/public.rs index 22f28c6..46e54c0 100644 --- a/src/public.rs +++ b/src/public.rs @@ -343,22 +343,14 @@ async fn submit_feedback( serde_html_form::from_bytes(&bytes).map_err(|e| cot::Error::internal(e.to_string()))?; if !crate::turnstile::verify(&db, form.cf_turnstile_response.as_deref()).await? { - return Redirect::new(format!( - "/client/{}?lang={}", - token_clone, - lang.code() - )) - .into_response(); + return Redirect::new(format!("/client/{}?lang={}", token_clone, lang.code())) + .into_response(); } if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? { if visit.status == "deleted" { - return Redirect::new(format!( - "/client/{}?lang={}", - token_clone, - lang.code() - )) - .into_response(); + return Redirect::new(format!("/client/{}?lang={}", token_clone, lang.code())) + .into_response(); } if visit.client_id.primary_key().unwrap() == client_id { visit.client_feedback = Some(form.feedback); @@ -552,8 +544,10 @@ async fn sitemap_xml(_request: Request, db: Database) -> cot::Result { domain = site_domain ); let mut resp = Response::new(cot::Body::fixed(body.into_bytes())); - resp.headers_mut() - .insert("content-type", "application/xml; charset=utf-8".parse().unwrap()); + resp.headers_mut().insert( + "content-type", + "application/xml; charset=utf-8".parse().unwrap(), + ); Ok(resp) }