implement account management

This commit is contained in:
insects 2025-02-06 13:28:26 +01:00
parent dbcac84794
commit 3e249e0a0d
8 changed files with 349 additions and 21 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@
data.js data.js
fish_data.js fish_data.js
beacon.db* beacon.db*
.env
.sqlx/

173
Cargo.lock generated
View file

@ -17,6 +17,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@ -161,12 +170,15 @@ dependencies = [
"chrono", "chrono",
"chrono-humanize", "chrono-humanize",
"maud", "maud",
"nanoid",
"serde", "serde",
"serde_json", "serde_json",
"serde_path_to_error", "serde_path_to_error",
"sqlx", "sqlx",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -375,7 +387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -943,6 +955,15 @@ version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 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]] [[package]]
name = "matchit" name = "matchit"
version = "0.8.4" version = "0.8.4"
@ -1025,6 +1046,25 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -1087,6 +1127,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -1248,6 +1294,50 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"
@ -1299,7 +1389,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1436,6 +1526,15 @@ dependencies = [
"digest", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -1760,7 +1859,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1783,6 +1882,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@ -1944,6 +2053,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@ -2014,6 +2153,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@ -2124,6 +2269,28 @@ dependencies = [
"wasite", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View file

@ -9,6 +9,7 @@ axum = { version = "0.8.1", features = ["macros"] }
chrono = "0.4.39" chrono = "0.4.39"
chrono-humanize = "0.2.3" chrono-humanize = "0.2.3"
maud = { version = "0.27.0", features = ["axum"] } maud = { version = "0.27.0", features = ["axum"] }
nanoid = "0.4.0"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138" serde_json = "1.0.138"
serde_path_to_error = "0.1.16" serde_path_to_error = "0.1.16"
@ -16,6 +17,9 @@ sqlx = { version = "0.8", features = [
"runtime-tokio", "runtime-tokio",
"tls-rustls-ring-webpki", "tls-rustls-ring-webpki",
"sqlite", "sqlite",
"migrate",
] } ] }
tokio = { version = "1.43.0", features = ["full"] } 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"] }

View file

@ -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);

32
src/db.rs Normal file
View file

@ -0,0 +1,32 @@
use sqlx::{Pool, Sqlite};
use crate::AppError;
pub struct Account {
account_id: Option<String>,
}
pub async fn insert_account(id: &str, pool: &Pool<Sqlite>) -> Result<(), AppError> {
sqlx::query!(
"
INSERT INTO accounts (account_id)
VALUES (?)
",
id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_account(id: &str, pool: &Pool<Sqlite>) -> Result<bool, AppError> {
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)
}
}

View file

@ -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 data::Data;
use maud::Markup; use maud::{html, Markup};
use nanoid::nanoid;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; 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 clock;
pub mod data; pub mod data;
pub mod db;
pub mod forecast; pub mod forecast;
pub mod templates; pub mod templates;
@ -37,21 +46,67 @@ where
} }
} }
#[axum::debug_handler] async fn root_handler() -> Result<Markup, AppError> {
Ok(templates::root())
}
async fn new_account_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
let id = nanoid!(10);
db::insert_account(&id, &state.pool).await?;
Ok(templates::new_account(id))
}
async fn main_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> { async fn main_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
Ok(templates::main_page(state, true)) Ok(templates::main_page(state))
}
async fn to_handler(
state: State<Arc<AppState>>,
query: Query<HashMap<String, String>>,
) -> Result<Response, AppError> {
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] #[tokio::main]
async fn 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() let pool = SqlitePoolOptions::new()
.max_connections(10) .max_connections(10)
.connect("sqlite://beacon.db") .connect("sqlite://beacon.db")
.await .await
.unwrap(); .unwrap();
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let app = Router::new() 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")) .nest_service("/static", ServeDir::new("static"))
.with_state(Arc::new(AppState { .with_state(Arc::new(AppState {
data: Data::new(), data: Data::new(),

View file

@ -21,7 +21,7 @@ pub fn layout(content: Markup) -> Markup {
} }
} }
pub fn main_page(state: State<Arc<AppState>>, with_layout: bool) -> Markup { pub fn main_page(state: State<Arc<AppState>>) -> Markup {
let meta = state.data.fish_with_meta(); let meta = state.data.fish_with_meta();
let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect(); let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect();
values.sort_by(|afish, bfish| { values.sort_by(|afish, bfish| {
@ -109,13 +109,64 @@ pub fn main_page(state: State<Arc<AppState>>, with_layout: bool) -> Markup {
} }
}; };
if with_layout {
layout(html! { layout(html! {
main hx-get="/" hx-trigger="every 10s" { main hx-get="" hx-trigger="every 10s" {
(template) (template)
} }
}) })
} else {
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" }
})
} }

View file

@ -96,3 +96,9 @@ section.alwaysup {
.catchpath:has(.tug) { .catchpath:has(.tug) {
margin-right: 9px; margin-right: 9px;
} }
p,
li {
max-width: 50em;
line-height: 1.5rem;
}