feat: improvements and ui stuff
also fixed some errors (and added `Unknown Album`s)
This commit is contained in:
parent
4a746a3371
commit
ace6ec1683
11 changed files with 528 additions and 80 deletions
|
|
@ -184,8 +184,6 @@ async fn handle_entry(
|
||||||
|
|
||||||
debug!("Inserted track {:?}", track.id);
|
debug!("Inserted track {:?}", track.id);
|
||||||
|
|
||||||
// TODO: figure out how to scan. steal from Gonic if we have to :3
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +192,8 @@ mod mp3;
|
||||||
|
|
||||||
#[instrument(skip(tx))]
|
#[instrument(skip(tx))]
|
||||||
async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result<i64, Report> {
|
async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result<i64, Report> {
|
||||||
|
let name = name.replace('\0', ""); // remove null bytes. they're not allowed :3
|
||||||
|
let name = name.trim();
|
||||||
debug!("Finding genre with name {name}");
|
debug!("Finding genre with name {name}");
|
||||||
let res = Genre::find()
|
let res = Genre::find()
|
||||||
.filter(genre::Column::Name.eq(name))
|
.filter(genre::Column::Name.eq(name))
|
||||||
|
|
@ -448,3 +448,7 @@ const fn mime_type_to_ext(mime: MimeType) -> &'static str {
|
||||||
MimeType::Gif => "gif",
|
MimeType::Gif => "gif",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_errors() {
|
||||||
|
STATUS.write().await.errors.clear();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::{borrow::Cow, result::Result};
|
||||||
|
|
||||||
use audiotags::{AudioTagEdit, FlacTag, MimeType, Tag};
|
use audiotags::{AudioTagEdit, FlacTag, MimeType, Tag};
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
|
|
@ -28,7 +28,9 @@ pub async fn handle(
|
||||||
let meta = { File::open(&path).await?.metadata().await? };
|
let meta = { File::open(&path).await?.metadata().await? };
|
||||||
|
|
||||||
let tag = {
|
let tag = {
|
||||||
let tag = Tag::default().read_from_path(&path)?;
|
let tag = Tag::default()
|
||||||
|
.read_from_path(&path)
|
||||||
|
.map_err(|e| Report::msg("check ID3 tags for invalid encodings").wrap_err(e))?;
|
||||||
|
|
||||||
FlacTag::from(tag)
|
FlacTag::from(tag)
|
||||||
};
|
};
|
||||||
|
|
@ -37,6 +39,15 @@ pub async fn handle(
|
||||||
|
|
||||||
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
||||||
|
|
||||||
|
if let Some(track) = Track::find()
|
||||||
|
.filter(track::Column::Path.eq(path.to_string_lossy()))
|
||||||
|
.one(tx)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("error searching for track").wrap_err(v))?
|
||||||
|
{
|
||||||
|
return Ok(track); // early exit if we already have this track. need to do an update check though :/
|
||||||
|
}
|
||||||
|
|
||||||
let mut am = track::ActiveModel::new();
|
let mut am = track::ActiveModel::new();
|
||||||
|
|
||||||
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
||||||
|
|
@ -81,7 +92,9 @@ pub async fn handle(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type).await?;
|
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("error getting cover art").wrap_err(v))?;
|
||||||
am.cover_art_id = Set(Some(cover_art.id));
|
am.cover_art_id = Set(Some(cover_art.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +104,10 @@ pub async fn handle(
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("Failed to convert meta len to i64"));
|
.expect("Failed to convert meta len to i64"));
|
||||||
|
|
||||||
let model = Track::insert(am).exec_with_returning(tx).await?;
|
let model = Track::insert(am)
|
||||||
|
.exec_with_returning(tx)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("couldn't add track").wrap_err(v))?;
|
||||||
|
|
||||||
Ok(model)
|
Ok(model)
|
||||||
}
|
}
|
||||||
|
|
@ -103,9 +119,11 @@ async fn find_album(
|
||||||
tag: &FlacTag,
|
tag: &FlacTag,
|
||||||
state: Arc<RwLock<ScanState>>,
|
state: Arc<RwLock<ScanState>>,
|
||||||
) -> Result<album::Model, Report> {
|
) -> Result<album::Model, Report> {
|
||||||
let Some(album) = tag.album() else {
|
let album = tag.album().unwrap_or(audiotags::types::Album {
|
||||||
return Err(Report::msg("Couldn't get album name from tag"));
|
title: "Unknown Album",
|
||||||
};
|
artist: None,
|
||||||
|
cover: None,
|
||||||
|
});
|
||||||
|
|
||||||
// if not, search by name
|
// if not, search by name
|
||||||
let search = Album::find()
|
let search = Album::find()
|
||||||
|
|
@ -128,8 +146,23 @@ async fn find_album(
|
||||||
|
|
||||||
let genre = tag.genre();
|
let genre = tag.genre();
|
||||||
if let Some(genre) = genre {
|
if let Some(genre) = genre {
|
||||||
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
if genre.contains('\0') {
|
||||||
am.genre_ids = Set(Some(vec![genre_id]));
|
let genre_ids = genre
|
||||||
|
.split('\0')
|
||||||
|
.map(|genre| super::find_or_create_genre(tx, genre))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let genre_ids = futures::future::join_all(genre_ids).await;
|
||||||
|
|
||||||
|
let genre_ids = genre_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
am.genre_ids = Set(Some(genre_ids));
|
||||||
|
} else {
|
||||||
|
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
||||||
|
am.genre_ids = Set(Some(vec![genre_id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
am.song_count = Set(tag
|
am.song_count = Set(tag
|
||||||
.total_tracks()
|
.total_tracks()
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ pub async fn handle(
|
||||||
let meta = { File::open(&path).await?.metadata().await? };
|
let meta = { File::open(&path).await?.metadata().await? };
|
||||||
|
|
||||||
let tag = {
|
let tag = {
|
||||||
let tag = Tag::default().read_from_path(&path)?;
|
let tag = Tag::default()
|
||||||
|
.read_from_path(&path)
|
||||||
|
.map_err(|e| Report::msg("check ID3 tags for invalid encodings").wrap_err(e))?;
|
||||||
|
|
||||||
audiotags::Id3v2Tag::from(tag)
|
audiotags::Id3v2Tag::from(tag)
|
||||||
};
|
};
|
||||||
|
|
@ -37,6 +39,15 @@ pub async fn handle(
|
||||||
|
|
||||||
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
||||||
|
|
||||||
|
if let Some(track) = Track::find()
|
||||||
|
.filter(track::Column::Path.eq(path.to_string_lossy()))
|
||||||
|
.one(tx)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("error searching for track").wrap_err(v))?
|
||||||
|
{
|
||||||
|
return Ok(track); // early exit if we already have this track. need to do an update check though :/
|
||||||
|
}
|
||||||
|
|
||||||
let mut am = track::ActiveModel::new();
|
let mut am = track::ActiveModel::new();
|
||||||
|
|
||||||
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
am.title = Set(tag.title().unwrap_or(&stem).to_string());
|
||||||
|
|
@ -81,7 +92,9 @@ pub async fn handle(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type).await?;
|
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("error getting cover art").wrap_err(v))?;
|
||||||
am.cover_art_id = Set(Some(cover_art.id));
|
am.cover_art_id = Set(Some(cover_art.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +104,10 @@ pub async fn handle(
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("Failed to convert meta len to i64"));
|
.expect("Failed to convert meta len to i64"));
|
||||||
|
|
||||||
let model = Track::insert(am).exec_with_returning(tx).await?;
|
let model = Track::insert(am)
|
||||||
|
.exec_with_returning(tx)
|
||||||
|
.await
|
||||||
|
.map_err(|v| Report::msg("couldn't add track").wrap_err(v))?;
|
||||||
|
|
||||||
Ok(model)
|
Ok(model)
|
||||||
}
|
}
|
||||||
|
|
@ -103,9 +119,11 @@ async fn find_album(
|
||||||
tag: &Id3v2Tag,
|
tag: &Id3v2Tag,
|
||||||
state: Arc<RwLock<ScanState>>,
|
state: Arc<RwLock<ScanState>>,
|
||||||
) -> Result<album::Model, Report> {
|
) -> Result<album::Model, Report> {
|
||||||
let Some(album) = tag.album() else {
|
let album = tag.album().unwrap_or(audiotags::types::Album {
|
||||||
return Err(Report::msg("Couldn't get album name from tag"));
|
title: "Unknown Album",
|
||||||
};
|
artist: None,
|
||||||
|
cover: None,
|
||||||
|
});
|
||||||
|
|
||||||
// if not, search by name
|
// if not, search by name
|
||||||
let search = Album::find()
|
let search = Album::find()
|
||||||
|
|
@ -128,8 +146,23 @@ async fn find_album(
|
||||||
|
|
||||||
let genre = tag.genre();
|
let genre = tag.genre();
|
||||||
if let Some(genre) = genre {
|
if let Some(genre) = genre {
|
||||||
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
if genre.contains('\0') {
|
||||||
am.genre_ids = Set(Some(vec![genre_id]));
|
let genre_ids = genre
|
||||||
|
.split('\0')
|
||||||
|
.map(|genre| super::find_or_create_genre(tx, genre))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let genre_ids = futures::future::join_all(genre_ids).await;
|
||||||
|
|
||||||
|
let genre_ids = genre_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
am.genre_ids = Set(Some(genre_ids));
|
||||||
|
} else {
|
||||||
|
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
||||||
|
am.genre_ids = Set(Some(vec![genre_id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
am.song_count = Set(tag
|
am.song_count = Set(tag
|
||||||
.total_tracks()
|
.total_tracks()
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,102 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{
|
||||||
|
scan::{get_scan_status, ScanStatus},
|
||||||
|
utils::db::DbTxn,
|
||||||
|
};
|
||||||
use entities::{
|
use entities::{
|
||||||
prelude::{User, UserSession},
|
prelude::{User, UserSession},
|
||||||
user::Model as UserModel,
|
user::Model as UserModel,
|
||||||
|
user_session,
|
||||||
};
|
};
|
||||||
use nate::Nate;
|
use nate::Nate;
|
||||||
use poem::{
|
use poem::{
|
||||||
http::{header, StatusCode},
|
http::{header, StatusCode},
|
||||||
session::Session,
|
session::Session,
|
||||||
web::Data,
|
web::Data,
|
||||||
Response,
|
Endpoint, EndpointExt, Response, Route,
|
||||||
};
|
};
|
||||||
use sea_orm::{EntityTrait, ModelTrait};
|
use sea_orm::{EntityTrait, ModelTrait};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
mod create_user;
|
||||||
|
mod delete_user;
|
||||||
|
mod scan;
|
||||||
|
|
||||||
|
pub fn build() -> Box<(dyn Endpoint<Output = Response> + 'static)> {
|
||||||
|
Route::new()
|
||||||
|
.at("/", dashboard)
|
||||||
|
.at("/create_user", create_user::create_user)
|
||||||
|
.at("/delete_user", delete_user::delete_user)
|
||||||
|
.at("/scan", scan::scan)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
|
let (_, user) = match get_session(txn, cookie).await {
|
||||||
|
Ok((session, user)) => (session, user),
|
||||||
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = cookie.get::<String>("message");
|
||||||
|
cookie.remove("message");
|
||||||
|
|
||||||
|
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!");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let scan_status = get_scan_status().await;
|
||||||
|
|
||||||
|
let scan = match scan_status {
|
||||||
|
Ok(scan) => scan,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get scan status: {e}");
|
||||||
|
|
||||||
|
cookie.set("message", "Internal server error occurred. Sorry!");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Response::builder().status(StatusCode::OK).body(
|
||||||
|
Dashboard {
|
||||||
|
username: user.name,
|
||||||
|
message,
|
||||||
|
admin: Some(Admin { users, scan }),
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Response::builder().status(StatusCode::OK).body(
|
||||||
|
Dashboard {
|
||||||
|
username: user.name,
|
||||||
|
message,
|
||||||
|
admin: None,
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_session(
|
||||||
|
txn: &DbTxn,
|
||||||
|
cookie: &Session,
|
||||||
|
) -> Result<(user_session::Model, UserModel), Response> {
|
||||||
let session_token = cookie.get::<String>("session_token");
|
let session_token = cookie.get::<String>("session_token");
|
||||||
|
|
||||||
if session_token.is_none() {
|
if session_token.is_none() {
|
||||||
|
|
@ -22,10 +104,10 @@ pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
|
|
||||||
cookie.set("message", "Please log in.");
|
cookie.set("message", "Please log in.");
|
||||||
|
|
||||||
return Response::builder()
|
return Err(Response::builder()
|
||||||
.status(StatusCode::FOUND)
|
.status(StatusCode::FOUND)
|
||||||
.header(header::LOCATION, "/login")
|
.header(header::LOCATION, "/login")
|
||||||
.finish();
|
.finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_token = session_token.expect("Failed to get session token");
|
let session_token = session_token.expect("Failed to get session token");
|
||||||
|
|
@ -35,8 +117,8 @@ pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
.one(&**txn)
|
.one(&**txn)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (_, user) = match session {
|
match session {
|
||||||
Ok(Some((session, Some(user)))) => (session, user),
|
Ok(Some((session, Some(user)))) => Ok((session, user)),
|
||||||
Ok(Some((session, None))) => {
|
Ok(Some((session, None))) => {
|
||||||
cookie.clear();
|
cookie.clear();
|
||||||
|
|
||||||
|
|
@ -44,20 +126,20 @@ pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
|
|
||||||
cookie.set("message", "Invalid session. Please log in again.");
|
cookie.set("message", "Invalid session. Please log in again.");
|
||||||
|
|
||||||
return Response::builder()
|
Err(Response::builder()
|
||||||
.status(StatusCode::FOUND)
|
.status(StatusCode::FOUND)
|
||||||
.header(header::LOCATION, "/login")
|
.header(header::LOCATION, "/login")
|
||||||
.finish();
|
.finish())
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
cookie.clear();
|
cookie.clear();
|
||||||
|
|
||||||
cookie.set("message", "Invalid session. Please log in again.");
|
cookie.set("message", "Invalid session. Please log in again.");
|
||||||
|
|
||||||
return Response::builder()
|
Err(Response::builder()
|
||||||
.status(StatusCode::FOUND)
|
.status(StatusCode::FOUND)
|
||||||
.header(header::LOCATION, "/login")
|
.header(header::LOCATION, "/login")
|
||||||
.finish();
|
.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -70,50 +152,11 @@ pub async fn dashboard(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
"Internal server error occurred. Sorry! Please log in again.",
|
"Internal server error occurred. Sorry! Please log in again.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return Response::builder()
|
Err(Response::builder()
|
||||||
.status(StatusCode::FOUND)
|
.status(StatusCode::FOUND)
|
||||||
.header(header::LOCATION, "/login")
|
.header(header::LOCATION, "/login")
|
||||||
.finish();
|
.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(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,4 +171,5 @@ pub struct Dashboard {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Admin {
|
pub struct Admin {
|
||||||
pub users: Vec<UserModel>,
|
pub users: Vec<UserModel>,
|
||||||
|
pub scan: ScanStatus,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
101
rave/src/ui/dashboard/create_user.rs
Normal file
101
rave/src/ui/dashboard/create_user.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
use entities::{prelude::User, user};
|
||||||
|
use poem::{
|
||||||
|
http::{header, StatusCode},
|
||||||
|
session::Session,
|
||||||
|
web::{Data, Form},
|
||||||
|
Response,
|
||||||
|
};
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
use crate::{ui::dashboard::get_session, utils::db::DbTxn};
|
||||||
|
|
||||||
|
#[poem::handler]
|
||||||
|
pub async fn create_user(
|
||||||
|
Data(txn): Data<&DbTxn>,
|
||||||
|
cookie: &Session,
|
||||||
|
Form(user_params): Form<CreateUser>,
|
||||||
|
) -> Response {
|
||||||
|
let (_, user) = match get_session(txn, cookie).await {
|
||||||
|
Ok((session, user)) => (session, user),
|
||||||
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !user.is_admin {
|
||||||
|
cookie.set("message", "ERROR: You are not an admin.");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = User::find()
|
||||||
|
.filter(user::Column::Name.eq(&user_params.username))
|
||||||
|
.one(&**txn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match existing {
|
||||||
|
Ok(None) => {}
|
||||||
|
Ok(Some(u)) => {
|
||||||
|
warn!(
|
||||||
|
"Found existing user {} while trying to add a new user {}",
|
||||||
|
u.name, user_params.username
|
||||||
|
);
|
||||||
|
cookie.set("message", "ERROR: User already exists.");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to find user: {e}");
|
||||||
|
cookie.set(
|
||||||
|
"message",
|
||||||
|
"ERROR: Internal error while checking for existing user.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_user = user::ActiveModel {
|
||||||
|
name: Set(user_params.username),
|
||||||
|
password: Set(user_params.password),
|
||||||
|
is_admin: Set(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_user = User::insert(new_user).exec(&**txn).await;
|
||||||
|
|
||||||
|
match new_user {
|
||||||
|
Ok(_) => {
|
||||||
|
cookie.set("message", "User created successfully.");
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create user: {e}");
|
||||||
|
cookie.set("message", "ERROR: Internal error while creating user.");
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateUser {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
92
rave/src/ui/dashboard/delete_user.rs
Normal file
92
rave/src/ui/dashboard/delete_user.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
use entities::{prelude::User, user};
|
||||||
|
use poem::{
|
||||||
|
http::{header, StatusCode},
|
||||||
|
session::Session,
|
||||||
|
web::{Data, Query},
|
||||||
|
Response,
|
||||||
|
};
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
use crate::{ui::dashboard::get_session, utils::db::DbTxn};
|
||||||
|
|
||||||
|
#[poem::handler]
|
||||||
|
pub async fn delete_user(
|
||||||
|
Data(txn): Data<&DbTxn>,
|
||||||
|
cookie: &Session,
|
||||||
|
Query(user_params): Query<DeleteUser>,
|
||||||
|
) -> Response {
|
||||||
|
let (_, user) = match get_session(txn, cookie).await {
|
||||||
|
Ok((session, user)) => (session, user),
|
||||||
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !user.is_admin {
|
||||||
|
cookie.set("message", "ERROR: You are not an admin.");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = User::find()
|
||||||
|
.filter(user::Column::Name.eq(&user_params.username))
|
||||||
|
.one(&**txn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let u = match existing {
|
||||||
|
Ok(None) => {
|
||||||
|
warn!(
|
||||||
|
"Found no user while trying to delete a user {}",
|
||||||
|
user_params.username
|
||||||
|
);
|
||||||
|
cookie.set("message", "ERROR: User does not exist.");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
Ok(Some(u)) => u,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to find user: {e}");
|
||||||
|
cookie.set(
|
||||||
|
"message",
|
||||||
|
"ERROR: Internal error while checking for existing user.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = u.delete(&**txn).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => {
|
||||||
|
cookie.set("message", "Successfully deleted user.");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to delete user: {e}");
|
||||||
|
cookie.set("message", "ERROR: Internal error while deleting user.");
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DeleteUser {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
45
rave/src/ui/dashboard/scan.rs
Normal file
45
rave/src/ui/dashboard/scan.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use poem::{
|
||||||
|
http::{header, StatusCode, Uri},
|
||||||
|
session::Session,
|
||||||
|
web::Data,
|
||||||
|
Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ui::dashboard::get_session, utils::db::DbTxn};
|
||||||
|
|
||||||
|
#[poem::handler]
|
||||||
|
pub async fn scan(Data(txn): Data<&DbTxn>, cookie: &Session, uri: &Uri) -> Response {
|
||||||
|
let (_, user) = match get_session(txn, cookie).await {
|
||||||
|
Ok((session, user)) => (session, user),
|
||||||
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.is_admin {
|
||||||
|
if let Some(query) = uri.query() {
|
||||||
|
match query {
|
||||||
|
"start" => {
|
||||||
|
crate::scan::start_scan().await;
|
||||||
|
cookie.set("message", "Scan started!");
|
||||||
|
}
|
||||||
|
|
||||||
|
"clearErrors" => {
|
||||||
|
crate::scan::clear_errors().await;
|
||||||
|
cookie.set("message", "Errors cleared!");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
cookie.set("message", format!("Unknown command: {query}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cookie.set("message", "No command provided");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cookie.set("message", "You are not an admin!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, "/dashboard")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ use tracing::{debug, error};
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
pub async fn login_ui(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
pub async fn login_ui(Data(txn): Data<&DbTxn>, cookie: &Session) -> Response {
|
||||||
let message = cookie.get::<String>("message");
|
let message = cookie.get::<String>("message");
|
||||||
|
cookie.remove("message");
|
||||||
|
|
||||||
let Some(token) = cookie.get::<String>("session_token") else {
|
let Some(token) = cookie.get::<String>("session_token") else {
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
|
||||||
.at("/", index::index)
|
.at("/", index::index)
|
||||||
.at("/login", get(login::login_ui).post(login::login))
|
.at("/login", get(login::login_ui).post(login::login))
|
||||||
.at("/logout", get(logout::logout))
|
.at("/logout", get(logout::logout))
|
||||||
.at("/dashboard", dashboard::dashboard)
|
.nest("/dashboard", dashboard::build())
|
||||||
.nest("/css", StaticFilesEndpoint::new(path))
|
.nest("/css", StaticFilesEndpoint::new(path))
|
||||||
.with(CookieSession::new(
|
.with(CookieSession::new(
|
||||||
CookieConfig::new().name(RAVE_COOKIE_NAME),
|
CookieConfig::new().name(RAVE_COOKIE_NAME),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Rave | Dashboard</title>
|
<title>Rave | Dashboard</title>
|
||||||
<link rel="stylesheet" href="css/dashboard.css">
|
<link rel="stylesheet" href="css/dashboard.css">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -21,15 +25,58 @@
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
<h2>Admin Stuff</h2>
|
<h2>Admin Stuff</h2>
|
||||||
<p>This is where admin-facing stuff is. I do know.</p>
|
<p>This is where admin-facing stuff is. I do know.</p>
|
||||||
<div id="add-users">
|
<div id="admin-users">
|
||||||
<p>Users:</p>
|
<h3>Users:</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{%- for user in &admin.users {-%}
|
{%- for user in &admin.users {-%}
|
||||||
<li class="user">
|
<li class="list-item">
|
||||||
<p>{{ user.name }} - {{ user.is_admin }}</p>
|
<span>{{ user.name }} - {{ if user.is_admin { "Admin" } else { "User" } }}</span>
|
||||||
|
<span>{%- if
|
||||||
|
user.is_admin{-%}
|
||||||
|
<span class="user-disabled" title="You cannot delete an admin">Delete</span>
|
||||||
|
{%-} else {-%}
|
||||||
|
<a class="user-delete" title="Deletes the user"
|
||||||
|
href="/dashboard/delete_user?username={{ user.name }}">Delete</a>
|
||||||
|
{%-}-%}</span>
|
||||||
</li>
|
</li>
|
||||||
{%- } -%}
|
{%- } -%}
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>Add a user:</p>
|
||||||
|
<div id="admin-add-user" class="row">
|
||||||
|
<form method="post" action="/dashboard/create_user">
|
||||||
|
<input type="text" name="username" placeholder="Username">
|
||||||
|
<input type="password" name="password" placeholder="Password">
|
||||||
|
<input type="submit" value="Add User">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div id="admin-scan">
|
||||||
|
<h3>Scan for new files:</h3>
|
||||||
|
<div class="row"><a class="admin-scan" href="/dashboard/scan?start">Scan</a>
|
||||||
|
<span>—</span>
|
||||||
|
<a class="admin-scan" href="/dashboard">Refresh</a>
|
||||||
|
<span>—</span>
|
||||||
|
<a class="admin-scan" href="/dashboard/scan?clearErrors">Clear Errors</a>
|
||||||
|
</div>
|
||||||
|
<div id="admin-scan-results">
|
||||||
|
<p>Scanning: {{ admin.scan.scanning }}</p>
|
||||||
|
<p>Scanned: {{ admin.scan.count }}</p>
|
||||||
|
|
||||||
|
{%- if !admin.scan.errors.is_empty() {-%}
|
||||||
|
<p>Errors:</p>
|
||||||
|
<ul>
|
||||||
|
{%- for error in &admin.scan.errors {-%}
|
||||||
|
<li class="list-item">
|
||||||
|
<span>{{ error.additional }}: </span>
|
||||||
|
{%- for cause in error.report.chain() { -%}
|
||||||
|
<code>{{ cause }} — </code>
|
||||||
|
{%- } -%}
|
||||||
|
</li>
|
||||||
|
{%- } -%}
|
||||||
|
</ul>
|
||||||
|
{%-}-%}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- } -%}
|
{%- } -%}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
body {
|
body {
|
||||||
background-color: #0a0a0a;
|
background-color: #070707;
|
||||||
color: #f0f0f0;
|
color: #f0f0f0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
|
@ -14,32 +16,77 @@ h2 {
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
border: 2px solid #707070;
|
border: 2px solid #707070;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#admin {
|
#admin {
|
||||||
border: 2px solid #700070;
|
border: 2px solid #700070;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #002ae0;
|
color: #005ae0;
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #002ae0;
|
color: #005ae0;
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
color: #002ae0;
|
color: #005ae0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.user {
|
ul {
|
||||||
|
padding-left: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.list-item {
|
||||||
|
list-style: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-bottom: 1px solid #707070;
|
border-bottom: 1px solid #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.list-item>p {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-add-user {
|
||||||
|
padding: 5px;
|
||||||
|
border-bottom: 1px solid #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color: #f0f0f0;
|
||||||
|
border: 2px solid #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.user-delete {
|
||||||
|
color: #c00000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.user-disabled {
|
||||||
|
color: grey;
|
||||||
|
text-decoration: 1px underline grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue