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:
parent
dc5fdf4b90
commit
4a746a3371
29 changed files with 810 additions and 143 deletions
174
Cargo.lock
generated
174
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ pub mod genre;
|
|||
pub mod music_folder;
|
||||
pub mod track;
|
||||
pub mod user;
|
||||
pub mod user_session;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<super::user_session::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserSession.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
|||
32
entities/src/user_session.rs
Normal file
32
entities/src/user_session.rs
Normal 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 {}
|
||||
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
migration/src/m000008_create_user_session.rs
Normal file
64
migration/src/m000008_create_user_session.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::utils::db::DbTxn;
|
||||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::utils::db::DbTxn;
|
||||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::utils::db::DbTxn;
|
||||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::utils::db::DbTxn;
|
||||
use poem::web::Data;
|
||||
use poem_ext::db::DbTxn;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
131
rave/src/ui/dashboard.rs
Normal file
131
rave/src/ui/dashboard.rs
Normal 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
42
rave/src/ui/index.rs
Normal 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
157
rave/src/ui/login.rs
Normal 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
73
rave/src/ui/logout.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
90
rave/src/utils/db.rs
Normal file
90
rave/src/utils/db.rs
Normal 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)
|
||||
}
|
||||
38
rave/templates/dashboard.html
Normal file
38
rave/templates/dashboard.html
Normal 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
26
rave/templates/login.html
Normal 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
45
static/css/dashboard.css
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue