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

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 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<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> {
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]
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(),

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 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<Arc<AppState>>, 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" }
})
}