diff --git a/Cargo.lock b/Cargo.lock index c1257d6..8992cbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -926,15 +926,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -2663,25 +2654,6 @@ dependencies = [ "serde", ] -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "qrcodegen" version = "1.8.0" @@ -3946,7 +3918,6 @@ dependencies = [ "leptos", "leptos_i18n", "leptos_router", - "pulldown-cmark", "qrcodegen", "rand 0.9.4", "serde", @@ -4063,12 +4034,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -4393,7 +4358,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a468cd0..d722f4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.2.13" +version = "0.2.12" [workspace] resolver = "2" diff --git a/clients/web/Cargo.toml b/clients/web/Cargo.toml index 4b82427..1edb9eb 100644 --- a/clients/web/Cargo.toml +++ b/clients/web/Cargo.toml @@ -19,7 +19,6 @@ futures = "0.3" rand = "0.9" gloo-storage = "0.3" qrcodegen = "1.8" -pulldown-cmark = "0.13" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.118" diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index 1d4cc77..dcc1b7b 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -305,62 +305,6 @@ a:hover { text-decoration: underline; } .portal-error { color: var(--ui-red-accent); font-size: 0.875rem; margin-top: 0.5rem; } .portal-success { color: var(--ui-green-accent); font-size: 0.875rem; margin-top: 0.5rem; } -.flash-banner { - position: fixed; - top: 1.25rem; - left: 50%; - transform: translateX(-50%); - z-index: 500; - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem 1.25rem; - background: var(--ui-green-accent); - color: #f5edd8; - border-radius: 6px; - box-shadow: 0 4px 16px rgba(0,0,0,0.35); - font-family: var(--font-ui); - font-size: 0.95rem; - max-width: 90vw; - animation: flash-in 0.2s ease; -} -@keyframes flash-in { - from { opacity: 0; transform: translateX(-50%) translateY(-0.5rem); } - to { opacity: 1; transform: translateX(-50%) translateY(0); } -} -.flash-dismiss { - background: none; - border: none; - color: inherit; - cursor: pointer; - font-size: 1rem; - opacity: 0.75; - padding: 0; - line-height: 1; -} -.flash-dismiss:hover { opacity: 1; } - -.portal-danger-zone { - border: 1px solid rgba(122, 30, 42, 0.4); - background: rgba(122, 30, 42, 0.04); -} -.portal-danger-zone h2 { - color: var(--ui-red-accent); -} -.portal-danger-btn { - padding: 0.5rem 1.25rem; - font-family: var(--font-ui); - font-size: 0.9rem; - background: var(--ui-red-accent); - color: #f5edd8; - border: none; - border-radius: 4px; - cursor: pointer; - transition: opacity 0.15s; -} -.portal-danger-btn:hover { opacity: 0.85; } -.portal-danger-btn:disabled { opacity: 0.45; cursor: not-allowed; } - .portal-link { color: var(--ui-gold); text-decoration: none; @@ -2140,20 +2084,6 @@ a:hover { text-decoration: underline; } border-top: 1px solid rgba(200,164,72,0.12); } -.site-nav-infolinks { - margin: 2em 0 1em; - text-align: center; - font-size: 0.9rem; - color: rgba(200,164,72,0.4); - display: flex; - flex-direction: row; - align-items: center; -} - -.site-nav-infolinks > a { - width: 100%; -} - .site-nav-version { margin: 2em 0 1em; display: block; @@ -2163,91 +2093,3 @@ a:hover { text-decoration: underline; } letter-spacing: 0.06em; color: rgba(200,164,72,0.4); } - -/* ── Content pages (markdown-rendered) ─────────────────────────────────────── */ - -.content-page h1 { - font-family: var(--font-display); - font-size: 2rem; - font-weight: 600; - color: var(--ui-ink); - letter-spacing: 0.04em; - margin-bottom: 0.5rem; -} -.content-page h2 { - font-family: var(--font-display); - font-size: 1.4rem; - font-weight: 600; - color: var(--ui-ink); - margin: 1.75rem 0 0.5rem; - border-bottom: 1px solid rgba(200,164,72,0.25); - padding-bottom: 0.25rem; -} -.content-page h3 { - font-family: var(--font-display); - font-size: 1.1rem; - font-weight: 600; - color: var(--ui-ink); - margin: 1.25rem 0 0.4rem; -} -.content-page p { - line-height: 1.7; - margin-bottom: 0.9rem; - color: var(--ui-ink); -} -.content-page ul, -.content-page ol { - margin: 0.5rem 0 1rem 1.5rem; - line-height: 1.7; - color: var(--ui-ink); -} -.content-page li { - margin-bottom: 0.25rem; -} -.content-page a { - color: var(--ui-gold-dark); - text-decoration: underline; -} -.content-page a:hover { - color: var(--ui-ink); -} -.content-page code { - font-family: monospace; - background: rgba(0,0,0,0.07); - border-radius: 3px; - padding: 0.1em 0.35em; - font-size: 0.88em; -} -.content-page pre { - background: rgba(0,0,0,0.07); - border-radius: 5px; - padding: 1rem 1.25rem; - overflow-x: auto; - margin-bottom: 1rem; -} -.content-page pre code { - background: none; - padding: 0; -} -.content-page blockquote { - border-left: 3px solid rgba(200,164,72,0.5); - margin: 0.75rem 0; - padding: 0.25rem 1rem; - color: #665544; - font-style: italic; -} -.content-page table { - border-collapse: collapse; - width: 100%; - margin-bottom: 1rem; -} -.content-page th, -.content-page td { - border: 1px solid rgba(200,164,72,0.3); - padding: 0.4rem 0.75rem; - text-align: left; -} -.content-page th { - background: rgba(200,164,72,0.1); - font-weight: 600; -} diff --git a/clients/web/locales/en.json b/clients/web/locales/en.json index 1e5fbc2..03ba37c 100644 --- a/clients/web/locales/en.json +++ b/clients/web/locales/en.json @@ -140,14 +140,5 @@ "nickname_modal_sign_in": "Sign in", "nickname_modal_register": "Create account", "new_game": "New game", - "language": "Language", - "delete_account_title": "Danger zone", - "delete_account_btn": "Delete my account", - "delete_account_warning": "This action is irreversible. Your account will be permanently deleted.", - "delete_account_confirm_label": "Type your username to confirm:", - "delete_account_confirm_btn": "Delete permanently", - "delete_account_mismatch": "Username does not match.", - "account_deleted": "Your account has been permanently deleted.", - "about": "About", - "legal": "Legal notices" + "language": "Language" } diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json index 28ae43c..d429838 100644 --- a/clients/web/locales/fr.json +++ b/clients/web/locales/fr.json @@ -138,14 +138,5 @@ "nickname_modal_sign_in": "connectez-vous", "nickname_modal_register": "Créer un compte", "new_game": "Nouvelle partie", - "language": "Langue", - "delete_account_title": "Zone de danger", - "delete_account_btn": "Supprimer mon compte", - "delete_account_warning": "Cette action est irréversible. Votre compte sera définitivement supprimé.", - "delete_account_confirm_label": "Tapez votre nom d'utilisateur pour confirmer :", - "delete_account_confirm_btn": "Supprimer définitivement", - "delete_account_mismatch": "Le nom d'utilisateur ne correspond pas.", - "account_deleted": "Votre compte a été définitivement supprimé.", - "about": "À propos", - "legal": "Mentions légales" + "language": "Langue" } diff --git a/clients/web/pages/about/en.md b/clients/web/pages/about/en.md deleted file mode 100644 index 27a382e..0000000 --- a/clients/web/pages/about/en.md +++ /dev/null @@ -1,12 +0,0 @@ -# About - -This application allows you to play [trictrac](https://en.wikipedia.org/wiki/Trictrac) against a friend online or locally against a bot. - -The source code is available at [github.com/mmai/trictrac](https://github.com/mmai/trictrac) -The application is self-hosted and runs on a simple Raspberry Pi. - -## Contact & bug Report - -For any questions, bug reports, or feedback, you can contact me at rhumbs@rhumbs.fr. - -If you encounter an issue during gameplay, you can copy the context of a game by clicking _Take snapshot_ then paste the resulting code into your message, specifying the expected behavior and the incorrect behavior you observed. diff --git a/clients/web/pages/about/fr.md b/clients/web/pages/about/fr.md deleted file mode 100644 index 1c3ec74..0000000 --- a/clients/web/pages/about/fr.md +++ /dev/null @@ -1,12 +0,0 @@ -# À propos - -Cette application vous permet de jouer au [trictrac](https://fr.wikipedia.org/wiki/Trictrac) contre un ami en ligne ou localement contre un bot. - -Le code source est disponible sur [github.com/mmai/trictrac](https://github.com/mmai/trictrac). -L'application est auto hébergée et tourne sur un simple Raspberry Pi. - -## Contact et rapport de bogue - -Pour toute question, rapport de bogue ou retour d'expérience, vous pouvez me contacter à l'adresse rhumbs@rhumbs.fr. - -Si vous constatez une anomalie en cours de jeu, vous pouvez copier le contexte d'une partie en cliquant sur _Prendre un instantané_, puis coller le code obtenu dans le message, en précisant le comportement auquel vous vous attendiez, et le comportement erroné constaté. diff --git a/clients/web/pages/legal/en.md b/clients/web/pages/legal/en.md deleted file mode 100644 index ff72761..0000000 --- a/clients/web/pages/legal/en.md +++ /dev/null @@ -1,26 +0,0 @@ -# Legal Notices - -## Data and Privacy - -This site does not use third-party analytics or advertising trackers. - -If you create an account, your username, email address, and argon2-hashed password are stored in a database on our server. Your email address is used only to send you account-related messages (email verification, password reset). It is never shared with third parties. - -Game records (room codes, move history, outcomes) may be stored to display game history on your profile page. - -## Cookies and Sessions - -A session cookie is stored in your browser when you sign in. It is used solely to keep you authenticated and expires after 30 days of inactivity. - -## Contact - -The website is created by - -Henri Bourcereau\ -7 rue Lugeol\ -33000 Bordeaux\ -France - -It is hosted at the same address. - -You can contact me at rhumbs@rhumbs.fr diff --git a/clients/web/pages/legal/fr.md b/clients/web/pages/legal/fr.md deleted file mode 100644 index 43f85d5..0000000 --- a/clients/web/pages/legal/fr.md +++ /dev/null @@ -1,28 +0,0 @@ -# Mentions légales - -## Données et vie privée - -Ce site n'utilise pas d'outils d'analyse tiers ni de traceurs publicitaires. - -Si vous créez un compte, votre nom d'utilisateur, votre adresse e-mail et votre mot de passe haché (argon2) sont stockés dans une base de données sur notre serveur. Votre adresse e-mail est utilisée uniquement pour vous envoyer des messages liés à votre compte (vérification d'e-mail, réinitialisation de mot de passe). Elle n'est jamais partagée avec des tiers. - -Les enregistrements de parties (codes de salle, historique des coups, résultats) peuvent être conservés afin d'afficher l'historique des parties sur votre page de profil. - -Vous pouvez supprimer votre compte et la totalité des données associées depuis votre page de profil. - -## Cookies et sessions - -Un cookie de session est stocké dans votre navigateur lorsque vous vous connectez. Il sert uniquement à maintenir votre authentification et expire après 30 jours d'inactivité. - -## Contact - -Le site est réalisé par - -Henri Bourcereau\ -7 rue Lugeol\ -33000 Bordeaux\ -France - -Il est hébergé à la même adresse. - -Vous pouvez me contacter à l'adresse rhumbs@rhumbs.fr diff --git a/clients/web/pages/readme.txt b/clients/web/pages/readme.txt deleted file mode 100644 index ea3df35..0000000 --- a/clients/web/pages/readme.txt +++ /dev/null @@ -1 +0,0 @@ -Sync this folder to the PAGES_DIR directory of the server running `relay-server`. diff --git a/clients/web/src/api.rs b/clients/web/src/api.rs index 2452b67..d826165 100644 --- a/clients/web/src/api.rs +++ b/clients/web/src/api.rs @@ -64,12 +64,6 @@ pub struct GameDetail { pub participants: Vec, } -#[derive(Clone, Debug, Deserialize)] -pub struct PageContent { - pub title: String, - pub content: String, -} - // ── Request bodies ──────────────────────────────────────────────────────────── #[derive(Serialize)] @@ -147,19 +141,6 @@ pub async fn post_logout() -> Result<(), String> { } } -pub async fn delete_account() -> Result<(), String> { - let resp = gloo_net::http::Request::delete(&url("/auth/account")) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 204 { - Ok(()) - } else { - Err(format!("status {}", resp.status())) - } -} - pub async fn get_user_profile(username: &str) -> Result { let resp = gloo_net::http::Request::get(&url(&format!("/users/{username}"))) .credentials(web_sys::RequestCredentials::Include) @@ -261,18 +242,6 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(), } } -pub async fn get_page(slug: &str, lang: &str) -> Result { - let resp = gloo_net::http::Request::get(&url(&format!("/pages/{slug}?lang={lang}"))) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - Err(format!("status {}", resp.status())) - } -} - // ── Utilities ───────────────────────────────────────────────────────────────── /// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`. diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs index ba90a54..9288be3 100644 --- a/clients/web/src/app.rs +++ b/clients/web/src/app.rs @@ -21,9 +21,9 @@ use crate::game::trictrac::backend::TrictracBackend; use crate::game::trictrac::types::{GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState}; use crate::i18n::*; use crate::portal::{ - account::AccountPage, content_page::ContentPage, forgot_password::ForgotPasswordPage, - game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage, - reset_password::ResetPasswordPage, verify_email::VerifyEmailPage, + account::AccountPage, forgot_password::ForgotPasswordPage, game_detail::GameDetailPage, + lobby::LobbyPage, profile::ProfilePage, reset_password::ResetPasswordPage, + verify_email::VerifyEmailPage, }; use trictrac_store::CheckerMove; @@ -34,9 +34,6 @@ use std::collections::VecDeque; pub(crate) struct AnonNickname(pub RwSignal>); #[derive(Clone, Copy)] pub(crate) struct AuthEmailVerified(pub RwSignal); -/// One-shot message shown as a top banner and auto-dismissed after a few seconds. -#[derive(Clone, Copy)] -pub(crate) struct FlashMessage(pub RwSignal>); fn relay_url() -> String { #[cfg(debug_assertions)] @@ -185,8 +182,6 @@ pub fn App() -> impl IntoView { // Nickname chosen by an anonymous player; used instead of "Anonymous". let anon_nickname: RwSignal> = RwSignal::new(None); provide_context(AnonNickname(anon_nickname)); - let flash: RwSignal> = RwSignal::new(None); - provide_context(FlashMessage(flash)); spawn_local(async move { if let Ok(me) = api::get_me().await { auth_username.set(Some(me.username)); @@ -428,7 +423,6 @@ pub fn App() -> impl IntoView { view! { -
"Page not found."

}> @@ -438,7 +432,6 @@ pub fn App() -> impl IntoView { -
@@ -447,34 +440,6 @@ pub fn App() -> impl IntoView { } } -/// Fixed top banner that shows a flash message and auto-dismisses after 5 seconds. -#[component] -fn FlashBanner() -> impl IntoView { - let flash = use_context::() - .expect("FlashMessage context not found") - .0; - - Effect::new(move |_| { - if flash.get().is_some() { - spawn_local(async move { - gloo_timers::future::TimeoutFuture::new(5_000).await; - flash.set(None); - }); - } - }); - - move || { - flash.get().map(|msg| { - view! { -
- { msg } - -
- } - }) - } -} - /// Renders the full-screen game overlay, but only when the current route is "/". /// This lets the user navigate to profile/account pages while a game is running. #[component] @@ -694,13 +659,6 @@ fn SiteHamburger() -> impl IntoView { sidebar_open.set(false); }>{t!(i18n, replay_snapshot)} -
"v" {VERSION}
diff --git a/clients/web/src/portal/content_page.rs b/clients/web/src/portal/content_page.rs deleted file mode 100644 index f44e3c0..0000000 --- a/clients/web/src/portal/content_page.rs +++ /dev/null @@ -1,51 +0,0 @@ -use leptos::prelude::*; -use leptos_router::hooks::use_params_map; -use pulldown_cmark::{Options, Parser, html}; - -use crate::api; -use crate::i18n::*; - -#[component] -pub fn ContentPage() -> impl IntoView { - let params = use_params_map(); - let slug = move || params.read().get("slug").unwrap_or_default(); - let i18n = use_i18n(); - - let page = LocalResource::new(move || { - let s = slug(); - let lang = match i18n.get_locale() { - Locale::en => "en", - Locale::fr => "fr", - }; - async move { api::get_page(&s, lang).await } - }); - - view! { -
- {move || match page.get().map(|sw| sw.take()) { - None => view! { -

{t!(i18n, loading)}

- }.into_any(), - Some(Err(_)) => view! { -

"Page not found."

- }.into_any(), - Some(Ok(p)) => { - let html = md_to_html(&p.content); - view! { -
- }.into_any() - } - }} -
- } -} - -fn md_to_html(md: &str) -> String { - let mut opts = Options::empty(); - opts.insert(Options::ENABLE_TABLES); - opts.insert(Options::ENABLE_STRIKETHROUGH); - let parser = Parser::new_ext(md, opts); - let mut output = String::new(); - html::push_html(&mut output, parser); - output -} diff --git a/clients/web/src/portal/mod.rs b/clients/web/src/portal/mod.rs index 54a84d1..a270b5f 100644 --- a/clients/web/src/portal/mod.rs +++ b/clients/web/src/portal/mod.rs @@ -1,5 +1,4 @@ pub mod account; -pub mod content_page; pub mod forgot_password; pub mod game_detail; pub mod lobby; diff --git a/clients/web/src/portal/profile.rs b/clients/web/src/portal/profile.rs index ac11bd6..c727bbd 100644 --- a/clients/web/src/portal/profile.rs +++ b/clients/web/src/portal/profile.rs @@ -1,8 +1,7 @@ use leptos::prelude::*; -use leptos_router::{components::A, hooks::use_navigate, hooks::use_params_map}; +use leptos_router::{components::A, hooks::use_params_map}; use crate::api::{self, GameSummary, UserProfile}; -use crate::app::{AuthEmailVerified, FlashMessage}; use crate::i18n::*; #[component] @@ -31,8 +30,6 @@ pub fn ProfilePage() -> impl IntoView { #[component] fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { let i18n = use_i18n(); - let auth_username = - use_context::>>().expect("auth_username context not found"); let page = RwSignal::new(0i64); let games = LocalResource::new(move || { let u = username.clone(); @@ -49,9 +46,7 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { time_style: None, }; let joined = api::format_ts(profile.created_at, locale_tag, &date_format); - - let profile_username = profile.username.clone(); - let is_own_profile = move || auth_username.get().as_deref() == Some(&profile_username); + // let joined = api::format_ts(profile.created_at, locale_tag, &api::DateFormatOptions::date_only()); view! {
@@ -88,106 +83,6 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { } }}
- - {move || if is_own_profile() { - let uname = profile.username.clone(); - view! { }.into_any() - } else { - view! { }.into_any() - }} - } -} - -#[component] -fn DeleteAccountSection(username: String) -> impl IntoView { - let i18n = use_i18n(); - let auth_username = - use_context::>>().expect("auth_username context not found"); - let auth_email_verified = use_context::() - .expect("auth_email_verified context not found").0; - let flash = use_context::().expect("FlashMessage context not found").0; - let navigate = use_navigate(); - - let confirming = RwSignal::new(false); - let confirm_input = RwSignal::new(String::new()); - let error = RwSignal::new(String::new()); - let pending = RwSignal::new(false); - - view! { -
-

{t!(i18n, delete_account_title)}

- {move || if !confirming.get() { - view! { -
-

- {t!(i18n, delete_account_warning)} -

- -
- }.into_any() - } else { - // Define submit fresh each reactive call so the closure is FnMut-compatible. - let expected = username.clone(); - let nav = navigate.clone(); - let submit = move |ev: leptos::ev::SubmitEvent| { - ev.prevent_default(); - if pending.get() { return; } - error.set(String::new()); - - if confirm_input.get() != expected { - error.set(t_string!(i18n, delete_account_mismatch).to_string()); - return; - } - - pending.set(true); - let nav = nav.clone(); - wasm_bindgen_futures::spawn_local(async move { - match api::delete_account().await { - Ok(()) => { - auth_username.set(None); - auth_email_verified.set(false); - flash.set(Some(t_string!(i18n, account_deleted).to_string())); - nav("/", Default::default()); - } - Err(e) => { - error.set(e); - pending.set(false); - } - } - }); - }; - view! { -
-

- {t!(i18n, delete_account_warning)} -

- - - {move || if !error.get().is_empty() { - view! {

{ error.get() }

}.into_any() - } else { - view! { }.into_any() - }} -
- - -
- - }.into_any() - }} -
} } diff --git a/container/flake.lock b/container/flake.lock index d81bd13..073ffc3 100644 --- a/container/flake.lock +++ b/container/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1779467186, - "narHash": "sha256-nOesoDCiXcUftqbRBMz9tt4blI5PvljMWbm3kuCA+0s=", + "lastModified": 1778430510, + "narHash": "sha256-Ti+ZBvW6yrWWAg2szExVTwCd4qOJ3KlVr1tFHfyfi8Q=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b77b3de8775677f84492abe84635f87b0e153f0f", + "rev": "8fd9daa3db09ced9700431c5b7ad0e8ba199b575", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 93b33d8..62b1eac 100644 --- a/flake.nix +++ b/flake.nix @@ -56,7 +56,7 @@ frontendCargoDeps = rustPlatform.fetchCargoVendor { src = ./.; name = "trictrac-frontend-vendor"; - hash = "sha256-E3MJbEehbXni7B8fQPS8fSri4f2b0A33r2djiK81E2Y="; + hash = "sha256-neJh0ZQGa5LNY8vBu3kYkM+ARkXOW/EHx8sPBOsWsgE="; }; in final.stdenv.mkDerivation { @@ -103,7 +103,7 @@ trictrac = with final; rustPlatform.buildRustPackage { pname = "trictrac"; - version = "0.2.13"; # trictrac-version + version = "0.2.12"; # trictrac-version src = ./.; nativeBuildInputs = [ pkg-config ]; diff --git a/justfile b/justfile index 3ae77d1..bc78103 100644 --- a/justfile +++ b/justfile @@ -11,12 +11,9 @@ bump version: sed -i 's/version = "[0-9.]*"; # trictrac-version/version = "{{version}}"; # trictrac-version/' flake.nix git add Cargo.toml flake.nix git commit -m "chore: bump version to {{version}}" + git flow release start {{version}} @echo "Done. Finish with: git flow release finish {{version}}" -# Sync pages content to production server -pages-deploy: - rsync -av clients/web/pages/ raspberry:/var/lib/trictrac/pages/ - doc: cargo doc --no-deps shell: @@ -50,7 +47,7 @@ build: [working-directory: 'deploy'] run-relay: - PAGES_DIR=../clients/web/pages ./relay-server + ./relay-server build-relay: CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p relay-server --release diff --git a/module.nix b/module.nix index 28bec85..53f77c6 100644 --- a/module.nix +++ b/module.nix @@ -29,12 +29,6 @@ in description = "Web server protocol."; }; - pages_dir = mkOption { - type = types.str; - default = "/var/lib/trictrac/pages"; - description = "Directory containing content pages."; - }; - hostname = mkOption { type = types.str; default = "trictrac.localhost"; @@ -138,9 +132,9 @@ in # Explicit listen so this vhost isn't shadowed by a default_server # created by other virtual hosts with forceSSL = true. listen = [ - { addr = "0.0.0.0"; port = listenPort; ssl = withSSL; } - { addr = "[::]"; port = listenPort; ssl = withSSL; } - ]; + { addr = "0.0.0.0"; port = listenPort; ssl = withSSL; } + { addr = "[::]"; port = listenPort; ssl = withSSL; } + ]; locations."/" = { extraConfig = proxyConfig; proxyPass = "http://trictrac-api/"; @@ -201,7 +195,6 @@ in environment = { DATABASE_URL = "postgresql://${cfg.user}@127.0.0.1/${cfg.user}"; APP_URL = "${cfg.protocol}://${cfg.hostname}"; - PAGES_DIR = cfg.pages_dir; SMTP_HOST = cfg.smtp.host; SMTP_PORT = toString (if cfg.smtp.port != null then cfg.smtp.port else if cfg.smtp.tls then 465 else 1025); diff --git a/server/relay-server/src/db.rs b/server/relay-server/src/db.rs index 0b9c878..83b9f25 100644 --- a/server/relay-server/src/db.rs +++ b/server/relay-server/src/db.rs @@ -188,25 +188,6 @@ pub async fn update_password_hash(pool: &Pool, user_id: i64, hash: &str) -> Resu Ok(()) } -/// Permanently deletes a user and their auth data. -/// Game history rows are kept but de-associated (user_id set to NULL). -pub async fn delete_user(pool: &Pool, user_id: i64) -> Result<(), DbError> { - let client = pool.get().await?; - client - .execute( - "UPDATE game_participants SET user_id = NULL WHERE user_id = $1", - &[&user_id], - ) - .await?; - client - .execute("DELETE FROM email_tokens WHERE user_id = $1", &[&user_id]) - .await?; - client - .execute("DELETE FROM users WHERE id = $1", &[&user_id]) - .await?; - Ok(()) -} - // ── Email tokens ────────────────────────────────────────────────────────────── pub async fn create_email_token( diff --git a/server/relay-server/src/http.rs b/server/relay-server/src/http.rs index 0104c76..c8701fc 100644 --- a/server/relay-server/src/http.rs +++ b/server/relay-server/src/http.rs @@ -19,7 +19,7 @@ use axum::{ extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, - routing::{delete, get, post}, + routing::{get, post}, }; use axum_login::AuthSession; use rand::distributions::Alphanumeric; @@ -48,12 +48,10 @@ pub fn router() -> Router> { .route("/auth/resend-verification", post(resend_verification)) .route("/auth/forgot-password", post(forgot_password)) .route("/auth/reset-password", post(reset_password)) - .route("/auth/account", delete(delete_account)) .route("/users/{username}", get(user_profile)) .route("/users/{username}/games", get(user_games)) .route("/games/result", post(game_result)) .route("/games/{id}", get(game_detail)) - .route("/pages/{slug}", get(get_page)) } // ── Token generation ────────────────────────────────────────────────────────── @@ -287,16 +285,6 @@ async fn logout(mut auth_session: AuthSession) -> Result, - State(state): State>, -) -> Result { - let user = auth_session.user.clone().ok_or(AppError::Unauthorized)?; - auth_session.logout().await.map_err(|_| AppError::Internal)?; - db::delete_user(&state.db, user.id).await?; - Ok(StatusCode::NO_CONTENT) -} - async fn me(auth_session: AuthSession) -> Result { match auth_session.user { Some(user) => Ok(Json(MeResponse { @@ -547,66 +535,3 @@ async fn game_result( Ok(Json(GameResultResponse { game_record_id })) } - -// ── Static content pages ────────────────────────────────────────────────────── - -#[derive(Deserialize)] -struct LangQuery { - #[serde(default = "default_lang")] - lang: String, -} - -fn default_lang() -> String { - "en".to_string() -} - -#[derive(Serialize)] -struct PageResponse { - title: String, - content: String, -} - -async fn get_page( - Path(slug): Path, - Query(query): Query, - State(state): State>, -) -> Result { - // Reject slugs with path-traversal characters or unusual lengths. - if slug.is_empty() - || slug.len() > 64 - || !slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') - { - return Err(AppError::NotFound); - } - // Normalise lang to a safe identifier. - let lang = if !query.lang.is_empty() - && query.lang.len() <= 5 - && query.lang.chars().all(|c| c.is_ascii_alphabetic()) - { - query.lang.to_ascii_lowercase() - } else { - "en".to_string() - }; - - let base = std::path::Path::new(&state.pages_dir); - let primary = base.join(&slug).join(format!("{lang}.md")); - - let content = match tokio::fs::read_to_string(&primary).await { - Ok(c) => c, - Err(_) if lang != "en" => { - let fallback = base.join(&slug).join("en.md"); - tokio::fs::read_to_string(&fallback) - .await - .map_err(|_| AppError::NotFound)? - } - Err(_) => return Err(AppError::NotFound), - }; - - let title = content - .lines() - .find(|l| l.starts_with("# ")) - .map(|l| l[2..].trim().to_string()) - .unwrap_or_default(); - - Ok(Json(PageResponse { title, content })) -} diff --git a/server/relay-server/src/lobby.rs b/server/relay-server/src/lobby.rs index db8f57c..db1c4f8 100644 --- a/server/relay-server/src/lobby.rs +++ b/server/relay-server/src/lobby.rs @@ -63,18 +63,15 @@ pub struct AppState { pub db: Pool, /// SMTP mailer for email verification and password reset. pub mailer: Mailer, - /// Directory containing static content pages as `{slug}/{lang}.md` files. - pub pages_dir: String, } impl AppState { - pub fn new(db: Pool, mailer: Mailer, pages_dir: String) -> Self { + pub fn new(db: Pool, mailer: Mailer) -> Self { Self { rooms: Mutex::new(HashMap::new()), configs: RwLock::new(HashMap::new()), db, mailer, - pages_dir, } } } diff --git a/server/relay-server/src/main.rs b/server/relay-server/src/main.rs index 367ef98..32baf70 100644 --- a/server/relay-server/src/main.rs +++ b/server/relay-server/src/main.rs @@ -66,8 +66,7 @@ async fn main() { let auth_backend = AuthBackend::new(pool.clone()); let auth_layer = AuthManagerLayerBuilder::new(auth_backend, session_layer).build(); - let pages_dir = std::env::var("PAGES_DIR").unwrap_or_else(|_| "pages".to_string()); - let app_state = Arc::new(AppState::new(pool, mailer, pages_dir)); + let app_state = Arc::new(AppState::new(pool, mailer)); let watchdog_state = app_state.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1200)); // 20 Min @@ -87,7 +86,7 @@ async fn main() { .allow_origin(AllowOrigin::list([ "http://localhost:9091".parse().unwrap(), // unified web dev server ])) - .allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS]) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) .allow_headers([ HeaderName::from_static("content-type"), HeaderName::from_static("cookie"),