diff --git a/.gitignore b/.gitignore index cb9046c..fb2dae5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ data.js fish_data.js beacon.db* +.env +.sqlx/ diff --git a/Cargo.lock b/Cargo.lock index 52686c6..479d707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -161,12 +170,15 @@ dependencies = [ "chrono", "chrono-humanize", "maud", + "nanoid", "serde", "serde_json", "serde_path_to_error", "sqlx", "tokio", "tower-http", + "tracing", + "tracing-subscriber", ] [[package]] @@ -375,7 +387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -943,6 +955,15 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1025,6 +1046,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1087,6 +1127,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" @@ -1248,6 +1294,50 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ring" version = "0.17.8" @@ -1299,7 +1389,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1436,6 +1526,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1760,7 +1859,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1783,6 +1882,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1944,6 +2053,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2014,6 +2153,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2124,6 +2269,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index b4afcee..6a72965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ axum = { version = "0.8.1", features = ["macros"] } chrono = "0.4.39" chrono-humanize = "0.2.3" maud = { version = "0.27.0", features = ["axum"] } +nanoid = "0.4.0" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" serde_path_to_error = "0.1.16" @@ -16,6 +17,9 @@ sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls-ring-webpki", "sqlite", + "migrate", ] } tokio = { version = "1.43.0", features = ["full"] } -tower-http = { version = "0.6.2", features = ["fs"] } +tower-http = { version = "0.6.2", features = ["fs", "trace"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/migrations/20250206105906_initial.sql b/migrations/20250206105906_initial.sql new file mode 100644 index 0000000..005b476 --- /dev/null +++ b/migrations/20250206105906_initial.sql @@ -0,0 +1,11 @@ +-- Add migration script here +CREATE TABLE accounts (account_id VARCHAR(10) PRIMARY KEY); + +CREATE TABLE caught_fish ( + id INTEGER PRIMARY KEY, + account_id VARCHAR(10), + fish_id INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts (account_id) ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE INDEX fish_accounts ON caught_fish (account_id); diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..9d5d606 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,32 @@ +use sqlx::{Pool, Sqlite}; + +use crate::AppError; + +pub struct Account { + account_id: Option, +} + +pub async fn insert_account(id: &str, pool: &Pool) -> Result<(), AppError> { + sqlx::query!( + " + INSERT INTO accounts (account_id) + VALUES (?) + ", + id + ) + .execute(pool) + .await?; + Ok(()) +} + +pub async fn get_account(id: &str, pool: &Pool) -> Result { + let results = sqlx::query_as!(Account, "SELECT * FROM accounts WHERE account_id = ?", id) + .fetch_all(pool) + .await?; + + if results.len() == 1 { + Ok(true) + } else { + Ok(false) + } +} diff --git a/src/main.rs b/src/main.rs index 851fc2a..6ee73ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,22 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, + routing::get, + Router, +}; use data::Data; -use maud::Markup; +use maud::{html, Markup}; +use nanoid::nanoid; use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; -use tower_http::services::ServeDir; +use tower_http::{services::ServeDir, trace::TraceLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub mod clock; pub mod data; +pub mod db; pub mod forecast; pub mod templates; @@ -37,21 +46,67 @@ where } } -#[axum::debug_handler] +async fn root_handler() -> Result { + Ok(templates::root()) +} + +async fn new_account_handler(state: State>) -> Result { + let id = nanoid!(10); + db::insert_account(&id, &state.pool).await?; + Ok(templates::new_account(id)) +} + async fn main_handler(state: State>) -> Result { - Ok(templates::main_page(state, true)) + Ok(templates::main_page(state)) +} + +async fn to_handler( + state: State>, + query: Query>, +) -> Result { + let rejection_html = html! { + p { "The provided account ID doesn't exist. " } + }; + if let Some(id) = query.get("id") { + let exists = db::get_account(id, &state.pool).await?; + if exists { + Ok(Redirect::to(&format!("/{}", id)).into_response()) + } else { + Ok(rejection_html.into_response()) + } + } else { + Ok(rejection_html.into_response()) + } } #[tokio::main] async fn main() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + format!( + "{}=debug,tower_http=debug,axum::rejection=trace", + env!("CARGO_CRATE_NAME") + ) + .into() + }), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + let pool = SqlitePoolOptions::new() .max_connections(10) .connect("sqlite://beacon.db") .await .unwrap(); + sqlx::migrate!("./migrations").run(&pool).await.unwrap(); + let app = Router::new() - .route("/", get(main_handler)) + .route("/", get(root_handler).post(new_account_handler)) + .route("/to", get(to_handler)) + .route("/{id}", get(main_handler)) + .layer(TraceLayer::new_for_http()) .nest_service("/static", ServeDir::new("static")) .with_state(Arc::new(AppState { data: Data::new(), diff --git a/src/templates.rs b/src/templates.rs index a2853f5..720ff53 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -21,7 +21,7 @@ pub fn layout(content: Markup) -> Markup { } } -pub fn main_page(state: State>, with_layout: bool) -> Markup { +pub fn main_page(state: State>) -> Markup { let meta = state.data.fish_with_meta(); let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect(); values.sort_by(|afish, bfish| { @@ -109,13 +109,64 @@ pub fn main_page(state: State>, with_layout: bool) -> Markup { } }; - if with_layout { - layout(html! { - main hx-get="/" hx-trigger="every 10s" { - (template) - } - }) - } else { - template - } + layout(html! { + main hx-get="" hx-trigger="every 10s" { + (template) + } + }) +} + +pub fn root() -> Markup { + layout(html! { + h1 { "Beacon" } + p { + "Beacon is a website to track progress for collecting " i { "big fish" } " in Final Fantasy XIV. " + "To use it, you need an " b { "account ID" } ", which serves to remember which fish you've caught. " + "If you have an account ID, you can enter it here, or if you don't, create a new one. Usually, your " + "browser will remember your ID and automatically redirect you, though." + } + + form method="post" { + button type="submit" { "Generate a new account ID" } + } + + p { "or" } + + form method="get" action="/to" { + input name="id" type="text" placeholder="Enter your account ID..."; + button type="submit" { "Go to tracker" } + } + + h2 { "What is this about?" } + p { + "In Final Fantasy XIV, you can catch a number of elusive and rare fish called big fish. These can be quite tricky to even find -- most of them have a specific time window and only appear during certain weather patterns. This website helps with keeping track of all of that." + } + + h2 { "Why not use the other tracker website?" } + p { + "By all means! " a href="https://ff14fish.carbuncleplushy.com" { "The original tracker" } " is a great resource, and functionally superior to this website. " + "I mostly created this because of two reasons:" + } + ol { + li { "Everything is rendered on the server - meaning it doesn't eat away at your system resources (good for laptop players!)" } + li { "Your caught fish are synchronized across devices, and data doesn't get deleted when you clear your browser data" } + } + p { + "These were the two main reasons why I created this. This website couldn't exist without Carbuncle Plushy's tracker -- it uses the same data " + "that has been painstakingly compiled by hand! If you can, please support them." + } + }) +} + +pub fn new_account(id: String) -> Markup { + layout(html! { + p { + "Your new account ID has been created. Plase save it in a place you remember, like a password manager. If you lose it, you will be unable to " + "access your account." + } + + h1 { (id) } + + a href=(format!("/{}", id)) { "Click here to proceed to the tracker" } + }) } diff --git a/static/style.css b/static/style.css index e4c972a..2bbaf34 100644 --- a/static/style.css +++ b/static/style.css @@ -96,3 +96,9 @@ section.alwaysup { .catchpath:has(.tug) { margin-right: 9px; } + +p, +li { + max-width: 50em; + line-height: 1.5rem; +}