From 4a746a3371be25e5cafa774eac05f84aff98b5a5 Mon Sep 17 00:00:00 2001 From: Lyssieth Date: Sat, 14 Oct 2023 19:03:20 +0300 Subject: [PATCH] feat: basic UI stuff I think I gotta do a lot more, but just pushing to ctrl+s my code. --- Cargo.lock | 174 +++++-------------- entities/src/lib.rs | 1 + entities/src/prelude.rs | 1 + entities/src/user.rs | 11 +- entities/src/user_session.rs | 32 ++++ migration/src/lib.rs | 2 + migration/src/m000008_create_user_session.rs | 64 +++++++ rave/Cargo.toml | 2 +- rave/src/main.rs | 8 +- rave/src/rest/get_album.rs | 2 +- rave/src/rest/get_album_list.rs | 2 +- rave/src/rest/get_cover_art.rs | 2 +- rave/src/rest/get_license.rs | 2 +- rave/src/rest/get_music_folders.rs | 2 +- rave/src/rest/get_scan_status.rs | 2 +- rave/src/rest/ping.rs | 2 +- rave/src/rest/search3.rs | 2 +- rave/src/rest/start_scan.rs | 2 +- rave/src/rest/stream.rs | 2 +- rave/src/ui/dashboard.rs | 131 ++++++++++++++ rave/src/ui/index.rs | 42 +++++ rave/src/ui/login.rs | 157 +++++++++++++++++ rave/src/ui/logout.rs | 73 ++++++++ rave/src/ui/mod.rs | 31 +++- rave/src/utils.rs | 5 +- rave/src/utils/db.rs | 90 ++++++++++ rave/templates/dashboard.html | 38 ++++ rave/templates/login.html | 26 +++ static/css/dashboard.css | 45 +++++ 29 files changed, 810 insertions(+), 143 deletions(-) create mode 100644 entities/src/user_session.rs create mode 100644 migration/src/m000008_create_user_session.rs create mode 100644 rave/src/ui/dashboard.rs create mode 100644 rave/src/ui/index.rs create mode 100644 rave/src/ui/login.rs create mode 100644 rave/src/ui/logout.rs create mode 100644 rave/src/utils/db.rs create mode 100644 rave/templates/dashboard.html create mode 100644 rave/templates/login.html create mode 100644 static/css/dashboard.css diff --git a/Cargo.lock b/Cargo.lock index 12ee043..d60ab7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.5.0" @@ -489,6 +498,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecount" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7" + [[package]] name = "bytemuck" version = "1.14.0" @@ -644,12 +659,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.17.0" @@ -872,19 +881,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "digest" version = "0.10.7" @@ -1860,22 +1856,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806" [[package]] -name = "multer" -version = "2.1.0" +name = "nate" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +checksum = "af2bb36e8bfd08117c5aa89acdb44e7dc07d21eb069dfe610d68dde21a69970e" dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin 0.9.8", - "tokio", - "version_check", + "itoa", + "nate-derive", + "ryu", +] + +[[package]] +name = "nate-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66008435de5625aa93e40baa94c9578b697ba38b268b047d3771dada9131225c" +dependencies = [ + "blake2", + "darling", + "hex", + "nom", + "nom_locate", + "quote", + "syn 2.0.38", ] [[package]] @@ -1906,6 +1909,17 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom_locate" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" +dependencies = [ + "bytecount", + "memchr", + "nom", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2270,7 +2284,6 @@ dependencies = [ "hyper", "mime", "mime_guess", - "multer", "parking_lot", "percent-encoding", "pin-project-lite", @@ -2283,13 +2296,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "serde_yaml", "smallvec", - "tempfile", "thiserror", "time", "tokio", - "tokio-stream", "tokio-util", "tower", "tracing", @@ -2307,65 +2317,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "poem-ext" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdbd601810df4590c4f9bdf8f4c567ad9979d296bb3ebd75190a45fb8f22a21" -dependencies = [ - "itertools", - "paste", - "poem", - "poem-openapi", - "sea-orm", - "serde", - "tokio-shield", - "tracing", -] - -[[package]] -name = "poem-openapi" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62659dcc7ca09a525881300646f3b28e319889072e83cd16a2865ba024a185e" -dependencies = [ - "base64", - "bytes", - "derive_more", - "futures-util", - "indexmap 2.0.2", - "mime", - "num-traits", - "poem", - "poem-openapi-derive", - "quick-xml", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "thiserror", - "tokio", -] - -[[package]] -name = "poem-openapi-derive" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90bf699e87e95b8303f9b59684cf3b9a8fff840872b13cbe0680aa4330d5226" -dependencies = [ - "darling", - "http", - "indexmap 2.0.2", - "mime", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn 2.0.38", - "thiserror", -] - [[package]] name = "polling" version = "2.8.0" @@ -2524,9 +2475,9 @@ dependencies = [ "image", "md5", "migration", + "nate", "once_cell", "poem", - "poem-ext", "quick-xml", "sea-orm", "sentry", @@ -3152,19 +3103,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" -dependencies = [ - "indexmap 2.0.2", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1" version = "0.10.6" @@ -3721,16 +3659,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-shield" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3777cb82f12ea1f163052c757c39899d2cf11bc73b045ff513de6699db4b7751" -dependencies = [ - "futures", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -3983,12 +3911,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" - [[package]] name = "untrusted" version = "0.7.1" diff --git a/entities/src/lib.rs b/entities/src/lib.rs index e5a61ae..becfeae 100644 --- a/entities/src/lib.rs +++ b/entities/src/lib.rs @@ -9,3 +9,4 @@ pub mod genre; pub mod music_folder; pub mod track; pub mod user; +pub mod user_session; diff --git a/entities/src/prelude.rs b/entities/src/prelude.rs index 976d011..88c443d 100644 --- a/entities/src/prelude.rs +++ b/entities/src/prelude.rs @@ -7,3 +7,4 @@ pub use super::genre::Entity as Genre; pub use super::music_folder::Entity as MusicFolder; pub use super::track::Entity as Track; pub use super::user::Entity as User; +pub use super::user_session::Entity as UserSession; diff --git a/entities/src/user.rs b/entities/src/user.rs index ab0c550..5a3869d 100644 --- a/entities/src/user.rs +++ b/entities/src/user.rs @@ -17,6 +17,15 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::user_session::Entity")] + UserSession, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserSession.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/entities/src/user_session.rs b/entities/src/user_session.rs new file mode 100644 index 0000000..e0369ad --- /dev/null +++ b/entities/src/user_session.rs @@ -0,0 +1,32 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_session")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub token: String, + pub user_id: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 34b3be6..5a29bac 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -7,6 +7,7 @@ mod m000004_create_artist; mod m000005_create_genre; mod m000006_create_album; mod m000007_create_track; +mod m000008_create_user_session; pub struct Migrator; @@ -21,6 +22,7 @@ impl MigratorTrait for Migrator { Box::new(m000005_create_genre::Migration), Box::new(m000006_create_album::Migration), Box::new(m000007_create_track::Migration), + Box::new(m000008_create_user_session::Migration), ] } } diff --git a/migration/src/m000008_create_user_session.rs b/migration/src/m000008_create_user_session.rs new file mode 100644 index 0000000..64be157 --- /dev/null +++ b/migration/src/m000008_create_user_session.rs @@ -0,0 +1,64 @@ +use sea_orm_migration::prelude::*; + +use crate::m000001_create_user::User; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(UserSession::Table) + .if_not_exists() + .col( + ColumnDef::new(UserSession::Token) + .string() + .not_null() + .unique_key() + .primary_key(), + ) + .col(ColumnDef::new(UserSession::UserId).big_integer().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(UserSession::Table) + .col(UserSession::Token) + .to_owned(), + ) + .await?; + + manager + .create_foreign_key( + ForeignKey::create() + .from_tbl(UserSession::Table) + .from_col(UserSession::UserId) + .to_tbl(User::Table) + .to_col(User::Id) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(UserSession::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +pub enum UserSession { + Table, + Token, + UserId, +} diff --git a/rave/Cargo.toml b/rave/Cargo.toml index 19c8c4f..eecde03 100644 --- a/rave/Cargo.toml +++ b/rave/Cargo.toml @@ -19,7 +19,6 @@ poem = { version = "1.3.58", features = [ "xml", "tower-compat", ] } -poem-ext = "0.9.4" quick-xml = { version = "0.30.0", features = ["serialize"] } serde = { workspace = true } serde_json = "1.0.107" @@ -56,3 +55,4 @@ sentry = { version = "0.31.7", default-features = false, features = [ sentry-tracing = { version = "0.31.7", features = ["backtrace"] } blake3 = "1.5.0" image = "0.24.7" +nate = "0.4.0" diff --git a/rave/src/main.rs b/rave/src/main.rs index df16b68..2a5e81d 100644 --- a/rave/src/main.rs +++ b/rave/src/main.rs @@ -11,12 +11,12 @@ use std::time::Duration; use color_eyre::Result; use migration::{Migrator, MigratorTrait}; use poem::{ + http::StatusCode, listener::TcpListener, middleware::{self, TowerLayerCompatExt}, web::{CompressionAlgo, CompressionLevel}, Endpoint, EndpointExt, Request, Route, }; -use poem_ext::db::DbTransactionMiddleware; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use sentry::integrations::tower::NewSentryLayer; use tracing::{debug, info}; @@ -25,6 +25,7 @@ use tracing_subscriber::{ fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, }; +use utils::db::DbTransactionMiddleware; mod authentication; mod random_types; @@ -61,7 +62,10 @@ async fn main() -> Result<()> { let dbc = create_pool().await?; Migrator::up(&dbc, None).await?; - let route = route.with(DbTransactionMiddleware::new(dbc)); + let route = route.with( + DbTransactionMiddleware::new(dbc) + .with_filter(|resp| resp.status().is_success() || resp.status() == StatusCode::FOUND), + ); let server = create_server(); diff --git a/rave/src/rest/get_album.rs b/rave/src/rest/get_album.rs index e964a71..0dc0f58 100644 --- a/rave/src/rest/get_album.rs +++ b/rave/src/rest/get_album.rs @@ -4,9 +4,9 @@ use crate::{ utils, }; +use crate::utils::db::DbTxn; use entities::prelude::{Album, Artist, Genre, Track}; use poem::web::{Data, Query}; -use poem_ext::db::DbTxn; use sea_orm::{EntityTrait, ModelTrait}; use serde::Deserialize; use tracing::{error, instrument}; diff --git a/rave/src/rest/get_album_list.rs b/rave/src/rest/get_album_list.rs index 5b4f72e..fe5257d 100644 --- a/rave/src/rest/get_album_list.rs +++ b/rave/src/rest/get_album_list.rs @@ -1,5 +1,6 @@ #![allow(clippy::unused_async)] // todo: remove +use crate::utils::db::DbTxn; use entities::{ album, artist, genre, prelude::{Album, Artist, Genre}, @@ -8,7 +9,6 @@ use poem::{ web::{Data, Query}, Request, }; -use poem_ext::db::DbTxn; use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect}; use serde::Deserialize; use tracing::{error, instrument}; diff --git a/rave/src/rest/get_cover_art.rs b/rave/src/rest/get_cover_art.rs index 70e974c..2ceb18b 100644 --- a/rave/src/rest/get_cover_art.rs +++ b/rave/src/rest/get_cover_art.rs @@ -1,12 +1,12 @@ use std::{io::Cursor, path::PathBuf}; +use crate::utils::db::DbTxn; use entities::prelude::CoverArt; use poem::{ http::StatusCode, web::{Data, Query}, IntoResponse, Response, }; -use poem_ext::db::DbTxn; use sea_orm::EntityTrait; use serde::Deserialize; use tracing::{error, instrument}; diff --git a/rave/src/rest/get_license.rs b/rave/src/rest/get_license.rs index 63d4552..3c0b4b8 100644 --- a/rave/src/rest/get_license.rs +++ b/rave/src/rest/get_license.rs @@ -1,5 +1,5 @@ +use crate::utils::db::DbTxn; use poem::web::Data; -use poem_ext::db::DbTxn; use tracing::instrument; use crate::{ diff --git a/rave/src/rest/get_music_folders.rs b/rave/src/rest/get_music_folders.rs index f3af967..dd1d226 100644 --- a/rave/src/rest/get_music_folders.rs +++ b/rave/src/rest/get_music_folders.rs @@ -1,6 +1,6 @@ +use crate::utils::db::DbTxn; use entities::prelude::MusicFolder; use poem::web::Data; -use poem_ext::db::DbTxn; use sea_orm::EntityTrait; use tracing::instrument; diff --git a/rave/src/rest/get_scan_status.rs b/rave/src/rest/get_scan_status.rs index 7b37dd8..347ad1b 100644 --- a/rave/src/rest/get_scan_status.rs +++ b/rave/src/rest/get_scan_status.rs @@ -1,5 +1,5 @@ +use crate::utils::db::DbTxn; use poem::web::Data; -use poem_ext::db::DbTxn; use tracing::{error, instrument}; use crate::{ diff --git a/rave/src/rest/ping.rs b/rave/src/rest/ping.rs index 6db1903..9ef84b9 100644 --- a/rave/src/rest/ping.rs +++ b/rave/src/rest/ping.rs @@ -1,5 +1,5 @@ +use crate::utils::db::DbTxn; use poem::web::Data; -use poem_ext::db::DbTxn; use tracing::instrument; use crate::{ diff --git a/rave/src/rest/search3.rs b/rave/src/rest/search3.rs index eef4b7c..e500d3b 100644 --- a/rave/src/rest/search3.rs +++ b/rave/src/rest/search3.rs @@ -1,3 +1,4 @@ +use crate::utils::db::DbTxn; use color_eyre::Report; use entities::{ album, artist, @@ -5,7 +6,6 @@ use entities::{ track, }; use poem::web::{Data, Query}; -use poem_ext::db::DbTxn; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect}; use serde::Deserialize; use tracing::{error, instrument}; diff --git a/rave/src/rest/start_scan.rs b/rave/src/rest/start_scan.rs index 7d58468..00e7343 100644 --- a/rave/src/rest/start_scan.rs +++ b/rave/src/rest/start_scan.rs @@ -1,5 +1,5 @@ +use crate::utils::db::DbTxn; use poem::web::Data; -use poem_ext::db::DbTxn; use tracing::{error, instrument}; use crate::{ diff --git a/rave/src/rest/stream.rs b/rave/src/rest/stream.rs index d49c1a5..517d642 100644 --- a/rave/src/rest/stream.rs +++ b/rave/src/rest/stream.rs @@ -1,10 +1,10 @@ +use crate::utils::db::DbTxn; use entities::prelude::Track; use poem::{ http::StatusCode, web::{Data, Query}, IntoResponse, Response, }; -use poem_ext::db::DbTxn; use sea_orm::EntityTrait; use serde::Deserialize; use tracing::{error, instrument}; diff --git a/rave/src/ui/dashboard.rs b/rave/src/ui/dashboard.rs new file mode 100644 index 0000000..86a3f2a --- /dev/null +++ b/rave/src/ui/dashboard.rs @@ -0,0 +1,131 @@ +use crate::utils::db::DbTxn; +use entities::{ + prelude::{User, UserSession}, + user::Model as UserModel, +}; +use nate::Nate; +use poem::{ + http::{header, StatusCode}, + session::Session, + web::Data, + Response, +}; +use sea_orm::{EntityTrait, ModelTrait}; +use tracing::error; + +#[poem::handler] +pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response { + let session_token = cookie.get::("session_token"); + + if session_token.is_none() { + cookie.clear(); + + cookie.set("message", "Please log in."); + + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish(); + } + + let session_token = session_token.expect("Failed to get session token"); + + let session = UserSession::find_by_id(&session_token) + .find_also_related(User) + .one(&**txn) + .await; + + let (_, user) = match session { + Ok(Some((session, Some(user)))) => (session, user), + Ok(Some((session, None))) => { + cookie.clear(); + + let _ = session.delete(&**txn).await; + + cookie.set("message", "Invalid session. Please log in again."); + + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish(); + } + Ok(None) => { + cookie.clear(); + + cookie.set("message", "Invalid session. Please log in again."); + + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish(); + } + + Err(e) => { + error!("Failed to find session: {e}"); + + cookie.clear(); + + cookie.set( + "message", + "Internal server error occurred. Sorry! Please log in again.", + ); + + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish(); + } + }; + + if user.is_admin { + let users = User::find().all(&**txn).await; + + let users = match users { + Ok(users) => users, + Err(e) => { + error!("Failed to find users: {e}"); + + cookie.set( + "message", + "Internal server error occurred. Sorry! Please log in again.", + ); + + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/dashboard") + .finish(); + } + }; + + Response::builder().status(StatusCode::OK).body( + Dashboard { + username: user.name, + message: cookie.get::("message"), + admin: Some(Admin { users }), + } + .to_string(), + ) + } else { + Response::builder().status(StatusCode::OK).body( + Dashboard { + username: user.name, + message: cookie.get::("message"), + admin: None, + } + .to_string(), + ) + } +} + +#[derive(Nate)] +#[template(path = "templates/dashboard.html")] +pub struct Dashboard { + pub username: String, + pub message: Option, + pub admin: Option, +} + +#[derive(Debug, Clone)] +pub struct Admin { + pub users: Vec, +} diff --git a/rave/src/ui/index.rs b/rave/src/ui/index.rs new file mode 100644 index 0000000..e9aebdb --- /dev/null +++ b/rave/src/ui/index.rs @@ -0,0 +1,42 @@ +use crate::utils::db::DbTxn; +use entities::prelude::UserSession; +use poem::{ + http::{header, StatusCode}, + session::Session, + web::Data, + Response, +}; +use sea_orm::EntityTrait; +use tracing::error; + +#[poem::handler] +pub async fn index(Data(txn): Data<&DbTxn>, session_cookie: &Session) -> Response { + let Some(token) = session_cookie.get::("session_token") else { + return Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish(); + }; + + let session = UserSession::find_by_id(&token).one(&**txn).await; + + match session { + Ok(Some(_)) => Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/dashboard") + .finish(), + Ok(None) => { + session_cookie.purge(); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + Err(e) => { + error!("Failed to find session: {e}"); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .finish() + } + } +} diff --git a/rave/src/ui/login.rs b/rave/src/ui/login.rs new file mode 100644 index 0000000..c5be5ad --- /dev/null +++ b/rave/src/ui/login.rs @@ -0,0 +1,157 @@ +use crate::utils::db::DbTxn; +use entities::{ + prelude::{User, UserSession}, + user, user_session, +}; +use nate::Nate; +use poem::{ + http::{header, StatusCode}, + session::Session, + web::{Data, Form}, + Response, +}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set}; +use serde::Deserialize; +use time::format_description::well_known::Iso8601; +use tracing::{debug, error}; + +#[poem::handler] +pub async fn login_ui(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response { + let message = cookie.get::("message"); + let Some(token) = cookie.get::("session_token") else { + return Response::builder() + .status(StatusCode::OK) + .body(LoginUi { message }.to_string()); + }; + + let session = UserSession::find_by_id(&token).one(&**txn).await; + + match session { + Ok(Some(_)) => Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/dashboard") + .finish(), + Err(e) => { + cookie.purge(); + cookie.set( + "message", + "An internal server error occurred. Please try again. If this persists, contact the admin or debug it yourself if you're the admin.", + ); + + error!("Failed to find session: {e}"); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + Ok(None) => Response::builder() + .status(StatusCode::OK) + .body(LoginUi { message }.to_string()), + } +} + +#[derive(Deserialize)] +pub struct SigninParams { + username: String, + password: String, +} + +#[poem::handler] +pub async fn login( + Data(txn): Data<&DbTxn>, + session: &Session, + Form(params): Form, +) -> Response { + let user = User::find() + .filter(user::Column::Name.eq(params.username)) + .one(&**txn) + .await; + + match user { + Ok(Some(user)) => { + if user.password == params.password { + let current_time = time::OffsetDateTime::now_utc() + .format(&Iso8601::DEFAULT) + .expect("Failed to format time"); + let token = blake3::hash(current_time.as_bytes()).to_string(); + + let session_token = UserSession::insert(user_session::ActiveModel { + token: Set(token), + user_id: Set(user.id), + }) + .exec_with_returning(&**txn) + .await; + + debug!("Created session: {session_token:?}"); + + if let Ok(token) = session_token { + session.clear(); + session.set("session_token", token.token); + + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/dashboard") + .finish() + } else if let Err(e) = session_token { + error!("Failed to create session: {e}"); + session.clear(); + session.set( + "message", + "Internal server error occurred. Sorry! Please try again.", + ); + + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } else { + session.clear(); + session.set( + "message", + "Internal server error occurred. Sorry! Please try again.", + ); + + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + } else { + session.clear(); + session.set("message", "Invalid username or password. Please try again."); + + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + } + Ok(None) => { + session.clear(); + session.set("message", "Invalid username or password. Please try again."); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + Err(e) => { + error!("Failed to find user: {e}"); + session.clear(); + session.set( + "message", + "Internal server error occurred. Sorry! Please try again.", + ); + + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .finish() + } + } +} + +#[derive(Nate)] +#[template(path = "templates/login.html")] +struct LoginUi { + message: Option, +} diff --git a/rave/src/ui/logout.rs b/rave/src/ui/logout.rs new file mode 100644 index 0000000..03f6658 --- /dev/null +++ b/rave/src/ui/logout.rs @@ -0,0 +1,73 @@ +use crate::utils::db::DbTxn; +use entities::prelude::UserSession; +use poem::{ + http::{header, StatusCode}, + session::Session, + web::Data, + Response, +}; +use sea_orm::{EntityTrait, ModelTrait}; +use tracing::error; + +#[poem::handler] +pub async fn logout(Data(txn): Data<&DbTxn>, session: &Session) -> Response { + let session_token = session.get::("session_token"); + + if let Some(token) = session_token { + let user_session = UserSession::find_by_id(&token).one(&**txn).await; + + match user_session { + Ok(Some(user_session)) => { + let user_session = user_session.delete(&**txn).await; + + match user_session { + Ok(_) => { + session.purge(); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .header("X-Message", "Successfully logged out") + .finish() + } + Err(e) => { + error!("Failed to delete session: {e}"); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .header("X-Message", "Internal server error occurred. Sorry!") + .finish() + } + } + } + Err(e) => { + error!("Failed to find session: {e}"); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .header("X-Message", "Internal server error occurred. Sorry!") + .finish() + } + Ok(None) => { + session.purge(); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .header( + "X-Message", + "You were already not logged in. Cleared session anyway.", + ) + .finish() + } + } + } else { + session.purge(); + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .header( + "X-Message", + "You were already not logged in. Cleared session anyway.", + ) + .finish() + } +} diff --git a/rave/src/ui/mod.rs b/rave/src/ui/mod.rs index 3f5d09a..d66d34f 100644 --- a/rave/src/ui/mod.rs +++ b/rave/src/ui/mod.rs @@ -1,7 +1,34 @@ -use poem::{Endpoint, EndpointExt, Route}; +use poem::{ + endpoint::StaticFilesEndpoint, + get, + session::{CookieConfig, CookieSession}, + Endpoint, EndpointExt, Route, +}; +use tracing::debug; +mod dashboard; mod errors; +mod index; +mod login; +mod logout; pub fn build() -> Box> { - Route::new().at("/errors", errors::errors).boxed() + let working_directory = std::env::current_dir().expect("Failed to get current directory"); + + debug!("Working directory: {:?}", working_directory); + let path = working_directory.join("static/css"); + + Route::new() + .at("/errors", errors::errors) + .at("/", index::index) + .at("/login", get(login::login_ui).post(login::login)) + .at("/logout", get(logout::logout)) + .at("/dashboard", dashboard::dashboard) + .nest("/css", StaticFilesEndpoint::new(path)) + .with(CookieSession::new( + CookieConfig::new().name(RAVE_COOKIE_NAME), + )) + .boxed() } + +pub const RAVE_COOKIE_NAME: &str = "rave"; diff --git a/rave/src/utils.rs b/rave/src/utils.rs index 92feced..5fee22f 100644 --- a/rave/src/utils.rs +++ b/rave/src/utils.rs @@ -1,5 +1,4 @@ use entities::{prelude::User, user}; -use poem_ext::db::DbTxn; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use tracing::error; @@ -8,6 +7,10 @@ use crate::{ subsonic::{Error, SubsonicResponse}, }; +use self::db::DbTxn; + +pub mod db; + pub async fn verify_user( conn: DbTxn, auth: Authentication, diff --git a/rave/src/utils/db.rs b/rave/src/utils/db.rs new file mode 100644 index 0000000..714d69a --- /dev/null +++ b/rave/src/utils/db.rs @@ -0,0 +1,90 @@ +use poem::{http::StatusCode, Endpoint, IntoResponse, Middleware, Response}; +use sea_orm::{DatabaseConnection, DatabaseTransaction, TransactionTrait}; +use std::{io::ErrorKind, sync::Arc}; + +pub type DbTxn = Arc; + +pub type CheckFn = Arc bool + Send + Sync>; + +pub struct DbTransactionMiddleware { + db: DatabaseConnection, + check_fn: Option, +} + +impl DbTransactionMiddleware { + #[must_use] + pub fn new(db: DatabaseConnection) -> Self { + Self { db, check_fn: None } + } + + #[must_use] + pub fn with_filter(self, check_fn: F) -> Self + where + F: Fn(&Response) -> bool + Send + Sync + 'static, + { + Self { + check_fn: Some(Arc::new(check_fn)), + ..self + } + } +} + +impl Middleware for DbTransactionMiddleware { + type Output = DbTransactionMiddlewareEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + DbTransactionMiddlewareEndpoint { + inner: ep, + db: self.db.clone(), + check_fn: self.check_fn.clone(), + } + } +} + +pub struct DbTransactionMiddlewareEndpoint { + inner: E, + db: DatabaseConnection, + check_fn: Option, +} + +#[poem::async_trait] +impl Endpoint for DbTransactionMiddlewareEndpoint { + type Output = Response; + + async fn call(&self, mut req: poem::Request) -> Result { + let txn = Arc::new(self.db.begin().await.map_err(internal_server_error)?); + req.extensions_mut().insert(txn.clone()); + let result = self.inner.call(req).await; + let txn = Arc::try_unwrap(txn).map_err(|_| { + internal_server_error(std::io::Error::new( + ErrorKind::Other, + "transaction not yet dropped from body", + )) + })?; + match result { + Ok(resp) => { + let resp = resp.into_response(); + if let Some(check_fn) = &self.check_fn { + if check_fn(&resp) { + txn.commit().await.map_err(internal_server_error)?; + } else { + txn.rollback().await.map_err(internal_server_error)?; + } + } else if resp.is_success() { + txn.commit().await.map_err(internal_server_error)?; + } else { + txn.rollback().await.map_err(internal_server_error)?; + } + Ok(resp) + } + Err(err) => { + txn.rollback().await.map_err(internal_server_error)?; + Err(err) + } + } + } +} + +fn internal_server_error(e: E) -> poem::Error { + poem::Error::new(e, StatusCode::INTERNAL_SERVER_ERROR) +} diff --git a/rave/templates/dashboard.html b/rave/templates/dashboard.html new file mode 100644 index 0000000..3b26bea --- /dev/null +++ b/rave/templates/dashboard.html @@ -0,0 +1,38 @@ + + + + + + Rave | Dashboard + + + + +

Signed in as {{self.username}}

+ {%- if let Some(message) = &self.message {-%} +

{{message}}

+ {%- } -%} +

Log out

+
+

User Stuff

+

This is where user-facing stuff is. I don't know.

+
+ {%- if let Some(admin) = &self.admin {-%} +
+

Admin Stuff

+

This is where admin-facing stuff is. I do know.

+
+

Users:

+
    + {%- for user in &admin.users {-%} +
  • +

    {{ user.name }} - {{ user.is_admin }}

    +
  • + {%- } -%} +
+
+
+ {%- } -%} + + + \ No newline at end of file diff --git a/rave/templates/login.html b/rave/templates/login.html new file mode 100644 index 0000000..79970cc --- /dev/null +++ b/rave/templates/login.html @@ -0,0 +1,26 @@ + + + + + + Rave | Login + + + + + +

Login

+ {%- if let Some(message) = &self.message { -%} +

Message: {{message}}

+ {%- } -%} + +
+
+ + + + + +
+
+ \ No newline at end of file diff --git a/static/css/dashboard.css b/static/css/dashboard.css new file mode 100644 index 0000000..4a78bca --- /dev/null +++ b/static/css/dashboard.css @@ -0,0 +1,45 @@ +body { + background-color: #0a0a0a; + color: #f0f0f0; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +h2 { + margin-top: 2px; +} + +#main { + border: 2px solid #707070; +} + +#admin { + border: 2px solid #700070; +} + +a { + color: #002ae0; + text-decoration: none +} + +a:hover { + color: #002ae0; + text-decoration: underline +} + +a:visited { + color: #002ae0; + text-decoration: none; +} + +li.user { + display: inline-flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 5px; + border-bottom: 1px solid #707070; +} \ No newline at end of file