diff --git a/Cargo.lock b/Cargo.lock index 087796a..b45e9f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "axum-login" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fbc0d7bd2577dda9aa9cac096e53b30342725d8eea5798169ff2537a214f45" +checksum = "4012877d9672b7902aa6567960208756f68a09de81e988fa18fe369e92f90471" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index f699073..7612472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ panic = "abort" [dependencies] argon2 = { version = "0.5.3", features = ["std"] } -axum-login = "0.15.1" +axum-login = "0.15.3" bincode = "1.3.3" chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "4.5.4", features = ["derive", "env"] } diff --git a/src/model/display.rs b/src/model/display.rs index a5366db..f2c0cbb 100644 --- a/src/model/display.rs +++ b/src/model/display.rs @@ -1,17 +1,22 @@ use std::collections::VecDeque; +use serde::{Deserialize, Serialize}; + use super::song::{Plan, Song, Verse}; +#[derive(Serialize, Deserialize, Debug)] pub struct PlaylistEntry { pub song: Song, pub plan_name: Option, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct PlaylistVerseRef { pub song_index: usize, pub map_verse_index: usize, } +#[derive(Serialize, Deserialize, Debug)] pub struct Display { pub playlist: VecDeque, current: PlaylistVerseRef, diff --git a/src/model/song.rs b/src/model/song.rs index 7de430d..273d96e 100644 --- a/src/model/song.rs +++ b/src/model/song.rs @@ -5,8 +5,9 @@ use std::{ }; use regex::Regex; +use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Verse { // pub background: String, // url pub content: String, @@ -21,6 +22,7 @@ impl Verse { /// Sequence of verse names. pub type Plan = VecDeque; +#[derive(Serialize, Deserialize, Debug)] pub struct Song { pub name: String, pub verses: BTreeMap, @@ -64,14 +66,14 @@ impl FromStr for Song { // would probably best be done with an actual AST static COMMENT_REGEX: OnceLock = OnceLock::new(); - let comment_re = COMMENT_REGEX.get_or_init(|| Regex::new(r"(?s)#[^\n]*").unwrap()); + let comment_re = COMMENT_REGEX.get_or_init(|| Regex::new(r"(?s)\r?\n?#[^\r\n]*").unwrap()); let s = comment_re.replace_all(s, "").into_owned(); dbg!(&s); static HUNK_REGEX: OnceLock = OnceLock::new(); - let hunk_re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\n\r]\s*[\n\r]\s*").unwrap()); + let hunk_re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\r\n]\s*[\r\n]\s*").unwrap()); let mut hunks = VecDeque::new(); let mut last_end: usize = 0; @@ -170,6 +172,7 @@ A verse"# #[test] fn parses_song_with_comments() { let song: Song = r#"Song Title + # this is a comment A verse"# .parse() diff --git a/src/partials.rs b/src/partials.rs index 257f11b..15bb7af 100644 --- a/src/partials.rs +++ b/src/partials.rs @@ -1,4 +1,4 @@ -use crate::router::ReqResult; +use crate::{router::ReqResult, service::accounts::AuthSession}; use axum::response::Html; use maud::{html, Markup, PreEscaped, DOCTYPE}; @@ -33,7 +33,12 @@ pub fn foot() -> Markup { } } -pub fn page(title: &str, content: Markup) -> ReqResult> { +pub fn page( + title: &str, + content: Markup, + auth_session: Option, +) -> ReqResult> { + let current_user = auth_session.map(|s| s.user).flatten(); Ok(Html( html! { (head(title)) @@ -41,8 +46,13 @@ pub fn page(title: &str, content: Markup) -> ReqResult> { header class="drop-shadow border-b-2 border-surface0 bg-blue-500 flex overflow-x-scroll" { a class="flex p-2 text-3xl font-mono text-mauve opacity-80 hover:bg-mauve hover:text-bg" href="/" { "lyrs" } nav class="flex flex-1 justify-start" { - a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/auth/login" { "Login" } - a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/auth/register" { "Register" } + @if let Some(user) = current_user { + a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/dashboard" {( user.username )} + a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/accounts/logout" { "Logout" } + } @else { + a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/accounts/login" { "Login" } + a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/accounts/register" { "Register" } + } } } main class="flex flex-col flex-1 relative overflow-x-scroll bg-mantle" { diff --git a/src/router.rs b/src/router.rs index 23d4baa..21d7fec 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,10 +1,11 @@ use crate::partials::page; +use crate::service::accounts::AuthSession; use crate::user::User; use crate::{db, user}; use crate::{ file_watcher::FileWatcher, prelude::*, - service::{auth, static_files}, + service::{accounts, static_files}, state::State as AppState, }; use axum::extract::State; @@ -79,7 +80,7 @@ pub async fn router( }; let (static_file_service, static_file_watcher) = static_files::router(orl())?; - let auth_service = auth::router(state.clone()).unwrap(); + let accounts_service = accounts::router(state.clone()).unwrap(); let session_store = tower_sessions_sled_store::SledStore::new(state.db.tree("session")?); let session_layer = SessionManagerLayer::new(session_store); @@ -88,11 +89,11 @@ pub async fn router( let mut result = Router::new() .route("/dashboard", get(dashboard)) - .route_layer(login_required!(AppState, login_url = "/auth/login")) + .route_layer(login_required!(AppState, login_url = "/accounts/login")) .route("/", get(index)) .route("/about", get(about)) .route("/users", get(users)) - .nest_service("/auth", auth_service) + .nest_service("/accounts", accounts_service) .nest_service("/static", static_file_service) .layer(TraceLayer::new_for_http()) .layer(auth_layer) @@ -107,16 +108,16 @@ pub async fn router( Ok((result, watchers)) } -async fn index() -> ReqResult> { - page("index", html! { "Index" }) +async fn index(auth_session: Option) -> ReqResult> { + page("index", html! { "Index" }, auth_session) } -async fn about() -> ReqResult> { - page("about", html! { "About" }) +async fn about(auth_session: Option) -> ReqResult> { + page("about", html! { "About" }, auth_session) } -async fn dashboard() -> ReqResult> { - page("dashboard", html! { "Dashboard" }) +async fn dashboard(auth_session: Option) -> ReqResult> { + page("dashboard", html! { "Dashboard" }, auth_session) } async fn users(State(state): State) -> ReqResult { diff --git a/src/service.rs b/src/service.rs index 6c9da11..09f46f3 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,2 +1,2 @@ -pub mod auth; +pub mod accounts; pub mod static_files; diff --git a/src/service/auth.rs b/src/service/accounts.rs similarity index 86% rename from src/service/auth.rs rename to src/service/accounts.rs index 424924a..bcd0e63 100644 --- a/src/service/auth.rs +++ b/src/service/accounts.rs @@ -1,27 +1,27 @@ use crate::router::ReqResult; use crate::state::{Creds, State as AppState}; use crate::{partials::*, user::User}; +use crate::{prelude::*, user}; use axum::extract::State; use axum::http::StatusCode; use axum::response::{Html, IntoResponse, Redirect}; use axum::Form; use maud::html; use std::convert::Infallible; +use tracing::instrument; use axum::{ routing::{get, post}, Router, }; -use crate::{prelude::*, user}; - pub fn router(state: AppState) -> Result { Ok(Router::new() - .route("/logout", get(logout)) .route("/login", get(login)) .route("/login", post(authenticate)) .route("/register", get(register)) .route("/register", post(create_user)) + .route("/logout", get(logout)) .with_state(state)) } @@ -37,11 +37,11 @@ async fn login() -> ReqResult> { let subaction = html! { small class="mt-4" { "Need an account? " - a href="/auth/register" {"Get one"} + a href="/accounts/register" {"Get one"} "." } }; - page("login", center_hero_form("Login", form, subaction)) + page("login", center_hero_form("Login", form, subaction), None) } async fn register() -> ReqResult> { @@ -56,14 +56,14 @@ async fn register() -> ReqResult> { let subaction = html! { small class="mt-4" { "Already have an account? " - a href="/auth/login" {"Login"} + a href="/accounts/login" {"Login"} "." } }; - page("login", center_hero_form("Register", form, subaction)) + page("login", center_hero_form("Register", form, subaction), None) } -type AuthSession = axum_login::AuthSession; +pub type AuthSession = axum_login::AuthSession; #[instrument(skip(auth_session))] async fn authenticate( @@ -111,7 +111,8 @@ async fn create_user( pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse { match auth_session.logout().await { - Ok(_) => Redirect::to("/auth/login").into_response(), + Ok(Some(_u)) => Redirect::to("/accounts/login?done").into_response(), + Ok(None) => Redirect::to("/accounts/login?unauthenticated").into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } }