feat: basic UI stuff I think

I gotta do a lot more, but just pushing to
ctrl+s my code.
This commit is contained in:
Lys 2023-10-14 19:03:20 +03:00
parent dc5fdf4b90
commit 4a746a3371
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
29 changed files with 810 additions and 143 deletions

174
Cargo.lock generated
View file

@ -424,6 +424,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "blake3" name = "blake3"
version = "1.5.0" version = "1.5.0"
@ -489,6 +498,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytecount"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.14.0" version = "1.14.0"
@ -644,12 +659,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.17.0" version = "0.17.0"
@ -872,19 +881,6 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1860,22 +1856,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806" checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806"
[[package]] [[package]]
name = "multer" name = "nate"
version = "2.1.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" checksum = "af2bb36e8bfd08117c5aa89acdb44e7dc07d21eb069dfe610d68dde21a69970e"
dependencies = [ dependencies = [
"bytes", "itoa",
"encoding_rs", "nate-derive",
"futures-util", "ryu",
"http", ]
"httparse",
"log", [[package]]
"memchr", name = "nate-derive"
"mime", version = "0.4.2"
"spin 0.9.8", source = "registry+https://github.com/rust-lang/crates.io-index"
"tokio", checksum = "66008435de5625aa93e40baa94c9578b697ba38b268b047d3771dada9131225c"
"version_check", dependencies = [
"blake2",
"darling",
"hex",
"nom",
"nom_locate",
"quote",
"syn 2.0.38",
] ]
[[package]] [[package]]
@ -1906,6 +1909,17 @@ dependencies = [
"minimal-lexical", "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]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -2270,7 +2284,6 @@ dependencies = [
"hyper", "hyper",
"mime", "mime",
"mime_guess", "mime_guess",
"multer",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@ -2283,13 +2296,10 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"serde_yaml",
"smallvec", "smallvec",
"tempfile",
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"tokio-stream",
"tokio-util", "tokio-util",
"tower", "tower",
"tracing", "tracing",
@ -2307,65 +2317,6 @@ dependencies = [
"syn 2.0.38", "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]] [[package]]
name = "polling" name = "polling"
version = "2.8.0" version = "2.8.0"
@ -2524,9 +2475,9 @@ dependencies = [
"image", "image",
"md5", "md5",
"migration", "migration",
"nate",
"once_cell", "once_cell",
"poem", "poem",
"poem-ext",
"quick-xml", "quick-xml",
"sea-orm", "sea-orm",
"sentry", "sentry",
@ -3152,19 +3103,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -3721,16 +3659,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.14" version = "0.1.14"
@ -3983,12 +3911,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View file

@ -9,3 +9,4 @@ pub mod genre;
pub mod music_folder; pub mod music_folder;
pub mod track; pub mod track;
pub mod user; pub mod user;
pub mod user_session;

View file

@ -7,3 +7,4 @@ pub use super::genre::Entity as Genre;
pub use super::music_folder::Entity as MusicFolder; pub use super::music_folder::Entity as MusicFolder;
pub use super::track::Entity as Track; pub use super::track::Entity as Track;
pub use super::user::Entity as User; pub use super::user::Entity as User;
pub use super::user_session::Entity as UserSession;

View file

@ -17,6 +17,15 @@ pub struct Model {
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {} pub enum Relation {
#[sea_orm(has_many = "super::user_session::Entity")]
UserSession,
}
impl Related<super::user_session::Entity> for Entity {
fn to() -> RelationDef {
Relation::UserSession.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -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<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -7,6 +7,7 @@ mod m000004_create_artist;
mod m000005_create_genre; mod m000005_create_genre;
mod m000006_create_album; mod m000006_create_album;
mod m000007_create_track; mod m000007_create_track;
mod m000008_create_user_session;
pub struct Migrator; pub struct Migrator;
@ -21,6 +22,7 @@ impl MigratorTrait for Migrator {
Box::new(m000005_create_genre::Migration), Box::new(m000005_create_genre::Migration),
Box::new(m000006_create_album::Migration), Box::new(m000006_create_album::Migration),
Box::new(m000007_create_track::Migration), Box::new(m000007_create_track::Migration),
Box::new(m000008_create_user_session::Migration),
] ]
} }
} }

View file

@ -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,
}

View file

@ -19,7 +19,6 @@ poem = { version = "1.3.58", features = [
"xml", "xml",
"tower-compat", "tower-compat",
] } ] }
poem-ext = "0.9.4"
quick-xml = { version = "0.30.0", features = ["serialize"] } quick-xml = { version = "0.30.0", features = ["serialize"] }
serde = { workspace = true } serde = { workspace = true }
serde_json = "1.0.107" 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"] } sentry-tracing = { version = "0.31.7", features = ["backtrace"] }
blake3 = "1.5.0" blake3 = "1.5.0"
image = "0.24.7" image = "0.24.7"
nate = "0.4.0"

View file

@ -11,12 +11,12 @@ use std::time::Duration;
use color_eyre::Result; use color_eyre::Result;
use migration::{Migrator, MigratorTrait}; use migration::{Migrator, MigratorTrait};
use poem::{ use poem::{
http::StatusCode,
listener::TcpListener, listener::TcpListener,
middleware::{self, TowerLayerCompatExt}, middleware::{self, TowerLayerCompatExt},
web::{CompressionAlgo, CompressionLevel}, web::{CompressionAlgo, CompressionLevel},
Endpoint, EndpointExt, Request, Route, Endpoint, EndpointExt, Request, Route,
}; };
use poem_ext::db::DbTransactionMiddleware;
use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use sentry::integrations::tower::NewSentryLayer; use sentry::integrations::tower::NewSentryLayer;
use tracing::{debug, info}; use tracing::{debug, info};
@ -25,6 +25,7 @@ use tracing_subscriber::{
fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
Registry, Registry,
}; };
use utils::db::DbTransactionMiddleware;
mod authentication; mod authentication;
mod random_types; mod random_types;
@ -61,7 +62,10 @@ async fn main() -> Result<()> {
let dbc = create_pool().await?; let dbc = create_pool().await?;
Migrator::up(&dbc, None).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(); let server = create_server();

View file

@ -4,9 +4,9 @@ use crate::{
utils, utils,
}; };
use crate::utils::db::DbTxn;
use entities::prelude::{Album, Artist, Genre, Track}; use entities::prelude::{Album, Artist, Genre, Track};
use poem::web::{Data, Query}; use poem::web::{Data, Query};
use poem_ext::db::DbTxn;
use sea_orm::{EntityTrait, ModelTrait}; use sea_orm::{EntityTrait, ModelTrait};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};

View file

@ -1,5 +1,6 @@
#![allow(clippy::unused_async)] // todo: remove #![allow(clippy::unused_async)] // todo: remove
use crate::utils::db::DbTxn;
use entities::{ use entities::{
album, artist, genre, album, artist, genre,
prelude::{Album, Artist, Genre}, prelude::{Album, Artist, Genre},
@ -8,7 +9,6 @@ use poem::{
web::{Data, Query}, web::{Data, Query},
Request, Request,
}; };
use poem_ext::db::DbTxn;
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect}; use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};

View file

@ -1,12 +1,12 @@
use std::{io::Cursor, path::PathBuf}; use std::{io::Cursor, path::PathBuf};
use crate::utils::db::DbTxn;
use entities::prelude::CoverArt; use entities::prelude::CoverArt;
use poem::{ use poem::{
http::StatusCode, http::StatusCode,
web::{Data, Query}, web::{Data, Query},
IntoResponse, Response, IntoResponse, Response,
}; };
use poem_ext::db::DbTxn;
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};

View file

@ -1,5 +1,5 @@
use crate::utils::db::DbTxn;
use poem::web::Data; use poem::web::Data;
use poem_ext::db::DbTxn;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{

View file

@ -1,6 +1,6 @@
use crate::utils::db::DbTxn;
use entities::prelude::MusicFolder; use entities::prelude::MusicFolder;
use poem::web::Data; use poem::web::Data;
use poem_ext::db::DbTxn;
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use tracing::instrument; use tracing::instrument;

View file

@ -1,5 +1,5 @@
use crate::utils::db::DbTxn;
use poem::web::Data; use poem::web::Data;
use poem_ext::db::DbTxn;
use tracing::{error, instrument}; use tracing::{error, instrument};
use crate::{ use crate::{

View file

@ -1,5 +1,5 @@
use crate::utils::db::DbTxn;
use poem::web::Data; use poem::web::Data;
use poem_ext::db::DbTxn;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{

View file

@ -1,3 +1,4 @@
use crate::utils::db::DbTxn;
use color_eyre::Report; use color_eyre::Report;
use entities::{ use entities::{
album, artist, album, artist,
@ -5,7 +6,6 @@ use entities::{
track, track,
}; };
use poem::web::{Data, Query}; use poem::web::{Data, Query};
use poem_ext::db::DbTxn;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};

View file

@ -1,5 +1,5 @@
use crate::utils::db::DbTxn;
use poem::web::Data; use poem::web::Data;
use poem_ext::db::DbTxn;
use tracing::{error, instrument}; use tracing::{error, instrument};
use crate::{ use crate::{

View file

@ -1,10 +1,10 @@
use crate::utils::db::DbTxn;
use entities::prelude::Track; use entities::prelude::Track;
use poem::{ use poem::{
http::StatusCode, http::StatusCode,
web::{Data, Query}, web::{Data, Query},
IntoResponse, Response, IntoResponse, Response,
}; };
use poem_ext::db::DbTxn;
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};

131
rave/src/ui/dashboard.rs Normal file
View file

@ -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::<String>("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::<String>("message"),
admin: Some(Admin { users }),
}
.to_string(),
)
} else {
Response::builder().status(StatusCode::OK).body(
Dashboard {
username: user.name,
message: cookie.get::<String>("message"),
admin: None,
}
.to_string(),
)
}
}
#[derive(Nate)]
#[template(path = "templates/dashboard.html")]
pub struct Dashboard {
pub username: String,
pub message: Option<String>,
pub admin: Option<Admin>,
}
#[derive(Debug, Clone)]
pub struct Admin {
pub users: Vec<UserModel>,
}

42
rave/src/ui/index.rs Normal file
View file

@ -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::<String>("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()
}
}
}

157
rave/src/ui/login.rs Normal file
View file

@ -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::<String>("message");
let Some(token) = cookie.get::<String>("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<SigninParams>,
) -> 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<String>,
}

73
rave/src/ui/logout.rs Normal file
View file

@ -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::<String>("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()
}
}

View file

@ -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 errors;
mod index;
mod login;
mod logout;
pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> { pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
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";

View file

@ -1,5 +1,4 @@
use entities::{prelude::User, user}; use entities::{prelude::User, user};
use poem_ext::db::DbTxn;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::error; use tracing::error;
@ -8,6 +7,10 @@ use crate::{
subsonic::{Error, SubsonicResponse}, subsonic::{Error, SubsonicResponse},
}; };
use self::db::DbTxn;
pub mod db;
pub async fn verify_user( pub async fn verify_user(
conn: DbTxn, conn: DbTxn,
auth: Authentication, auth: Authentication,

90
rave/src/utils/db.rs Normal file
View file

@ -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<DatabaseTransaction>;
pub type CheckFn = Arc<dyn Fn(&Response) -> bool + Send + Sync>;
pub struct DbTransactionMiddleware {
db: DatabaseConnection,
check_fn: Option<CheckFn>,
}
impl DbTransactionMiddleware {
#[must_use]
pub fn new(db: DatabaseConnection) -> Self {
Self { db, check_fn: None }
}
#[must_use]
pub fn with_filter<F>(self, check_fn: F) -> Self
where
F: Fn(&Response) -> bool + Send + Sync + 'static,
{
Self {
check_fn: Some(Arc::new(check_fn)),
..self
}
}
}
impl<E: Endpoint> Middleware<E> for DbTransactionMiddleware {
type Output = DbTransactionMiddlewareEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
DbTransactionMiddlewareEndpoint {
inner: ep,
db: self.db.clone(),
check_fn: self.check_fn.clone(),
}
}
}
pub struct DbTransactionMiddlewareEndpoint<E> {
inner: E,
db: DatabaseConnection,
check_fn: Option<CheckFn>,
}
#[poem::async_trait]
impl<E: Endpoint> Endpoint for DbTransactionMiddlewareEndpoint<E> {
type Output = Response;
async fn call(&self, mut req: poem::Request) -> Result<Self::Output, poem::Error> {
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: std::error::Error + Send + Sync + 'static>(e: E) -> poem::Error {
poem::Error::new(e, StatusCode::INTERNAL_SERVER_ERROR)
}

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rave | Dashboard</title>
<link rel="stylesheet" href="css/dashboard.css">
</head>
<body>
<h1>Signed in as <code>{{self.username}}</code></h1>
{%- if let Some(message) = &self.message {-%}
<h2>{{message}}</h2>
{%- } -%}
<p><a href="logout">Log out</a></p>
<div id="main">
<h2>User Stuff</h2>
<p>This is where user-facing stuff is. I don't know.</p>
</div>
{%- if let Some(admin) = &self.admin {-%}
<div id="admin">
<h2>Admin Stuff</h2>
<p>This is where admin-facing stuff is. I do know.</p>
<div id="add-users">
<p>Users:</p>
<ul>
{%- for user in &admin.users {-%}
<li class="user">
<p>{{ user.name }} - {{ user.is_admin }}</p>
</li>
{%- } -%}
</ul>
</div>
</div>
{%- } -%}
</body>
</html>

26
rave/templates/login.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rave | Login</title>
<link rel="stylesheet" href="css/login.css">
</html>
<body>
<h1>Login</h1>
{%- if let Some(message) = &self.message { -%}
<p>Message: {{message}}</p>
{%- } -%}
<div>
<form action="/login" method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" placeholder="AzureDiamond" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" placeholder="hunter2" required>
<input type="submit" value="Login">
</form>
</div>
</body>

45
static/css/dashboard.css Normal file
View file

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