Adds settings endpoints
This commit is contained in:
parent
84921f7db3
commit
5c4631c673
5 changed files with 232 additions and 20 deletions
|
@ -15,6 +15,8 @@ branca = "0.10.1"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
futures-util = { version = "0.3" }
|
futures-util = { version = "0.3" }
|
||||||
getopts = "0.2.21"
|
getopts = "0.2.21"
|
||||||
|
headers = "0.4"
|
||||||
|
http = "1.1.0"
|
||||||
id3 = "1.7.0"
|
id3 = "1.7.0"
|
||||||
lewton = "0.10.2"
|
lewton = "0.10.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
|
@ -76,8 +78,6 @@ winres = "0.1"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = "15.3"
|
axum-test = "15.3"
|
||||||
bytes = "1.6.1"
|
bytes = "1.6.1"
|
||||||
headers = "0.4"
|
|
||||||
http = "1.1.0"
|
|
||||||
fs_extra = "1.2.0"
|
fs_extra = "1.2.0"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
use axum::Router;
|
use axum::{extract::FromRef, Router};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{self, App};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod auth;
|
||||||
|
mod error;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
pub fn make_router(app: App) -> Router {
|
pub fn make_router(app: App) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.nest("/api", api::router())
|
||||||
|
.with_state(app.clone())
|
||||||
.nest_service("/swagger", ServeDir::new(app.swagger_dir_path))
|
.nest_service("/swagger", ServeDir::new(app.swagger_dir_path))
|
||||||
.nest_service("/", ServeDir::new(app.web_dir_path))
|
.nest_service("/", ServeDir::new(app.web_dir_path))
|
||||||
// move |cfg: &mut ServiceConfig| {
|
|
||||||
// cfg.app_data(web::Data::new(app.index))
|
|
||||||
// .app_data(web::Data::new(app.config_manager))
|
|
||||||
// .app_data(web::Data::new(app.ddns_manager))
|
|
||||||
// .app_data(web::Data::new(app.lastfm_manager))
|
|
||||||
// .app_data(web::Data::new(app.playlist_manager))
|
|
||||||
// .app_data(web::Data::new(app.settings_manager))
|
|
||||||
// .app_data(web::Data::new(app.thumbnail_manager))
|
|
||||||
// .app_data(web::Data::new(app.user_manager))
|
|
||||||
// .app_data(web::Data::new(app.vfs_manager))
|
|
||||||
// .service(
|
|
||||||
// web::scope("/api")
|
|
||||||
// .configure(api::make_config())
|
|
||||||
// .wrap(NormalizePath::trim()),
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch(app: App) -> Result<(), std::io::Error> {
|
pub async fn launch(app: App) -> Result<(), std::io::Error> {
|
||||||
|
@ -39,3 +27,21 @@ pub async fn launch(app: App) -> Result<(), std::io::Error> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRef<App> for app::config::Manager {
|
||||||
|
fn from_ref(app: &App) -> Self {
|
||||||
|
app.config_manager.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<App> for app::user::Manager {
|
||||||
|
fn from_ref(app: &App) -> Self {
|
||||||
|
app.user_manager.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<App> for app::settings::Manager {
|
||||||
|
fn from_ref(app: &App) -> Self {
|
||||||
|
app.settings_manager.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
routing::{get, put},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{config, settings, user, App},
|
||||||
|
server::{dto, error::APIError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::auth::AdminRights;
|
||||||
|
|
||||||
|
pub fn router() -> Router<App> {
|
||||||
|
Router::new()
|
||||||
|
.route("/version", get(get_version))
|
||||||
|
.route("/initial_setup", get(get_initial_setup))
|
||||||
|
.route("/config", put(put_config))
|
||||||
|
.route("/settings", get(get_settings))
|
||||||
|
.route("/settings", put(put_settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_version() -> Json<dto::Version> {
|
||||||
|
let current_version = dto::Version {
|
||||||
|
major: dto::API_MAJOR_VERSION,
|
||||||
|
minor: dto::API_MINOR_VERSION,
|
||||||
|
};
|
||||||
|
Json(current_version)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_initial_setup(
|
||||||
|
State(user_manager): State<user::Manager>,
|
||||||
|
) -> Result<Json<dto::InitialSetup>, APIError> {
|
||||||
|
let initial_setup = {
|
||||||
|
let users = user_manager.list().await?;
|
||||||
|
let has_any_admin = users.iter().any(|u| u.is_admin());
|
||||||
|
dto::InitialSetup {
|
||||||
|
has_any_users: has_any_admin,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Json(initial_setup))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_config(
|
||||||
|
_admin_rights: AdminRights,
|
||||||
|
State(config_manager): State<config::Manager>,
|
||||||
|
Json(config): Json<dto::Config>,
|
||||||
|
) -> Result<(), APIError> {
|
||||||
|
config_manager.apply(&config.into()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_settings(
|
||||||
|
State(settings_manager): State<settings::Manager>,
|
||||||
|
_admin_rights: AdminRights,
|
||||||
|
) -> Result<Json<dto::Settings>, APIError> {
|
||||||
|
let settings = settings_manager.read().await?;
|
||||||
|
Ok(Json(settings.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_settings(
|
||||||
|
_admin_rights: AdminRights,
|
||||||
|
State(settings_manager): State<settings::Manager>,
|
||||||
|
Json(new_settings): Json<dto::NewSettings>,
|
||||||
|
) -> Result<(), APIError> {
|
||||||
|
settings_manager
|
||||||
|
.amend(&new_settings.to_owned().into())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
86
src/server/axum/auth.rs
Normal file
86
src/server/axum/auth.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::{FromRef, FromRequestParts, Query},
|
||||||
|
};
|
||||||
|
use headers::authorization::{Bearer, Credentials};
|
||||||
|
use http::request::Parts;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::user,
|
||||||
|
server::{dto, error::APIError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Auth {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for Auth
|
||||||
|
where
|
||||||
|
user::Manager: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = APIError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let user_manager = user::Manager::from_ref(app);
|
||||||
|
|
||||||
|
let header_token = parts
|
||||||
|
.headers
|
||||||
|
.get(http::header::AUTHORIZATION)
|
||||||
|
.and_then(Bearer::decode)
|
||||||
|
.map(|b| b.token().to_string());
|
||||||
|
|
||||||
|
let query_token = Query::<dto::AuthQueryParameters>::try_from_uri(&parts.uri)
|
||||||
|
.ok()
|
||||||
|
.map(|p| p.auth_token.to_string());
|
||||||
|
|
||||||
|
let Some(token) = query_token.or(header_token) else {
|
||||||
|
return Err(APIError::AuthenticationRequired);
|
||||||
|
};
|
||||||
|
|
||||||
|
let authorization = user_manager
|
||||||
|
.authenticate(
|
||||||
|
&user::AuthToken(token),
|
||||||
|
user::AuthorizationScope::PolarisAuth,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Auth {
|
||||||
|
username: authorization.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AdminRights {
|
||||||
|
auth: Option<Auth>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for AdminRights
|
||||||
|
where
|
||||||
|
user::Manager: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = APIError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let user_manager = user::Manager::from_ref(app);
|
||||||
|
|
||||||
|
let auth_future = Auth::from_request_parts(parts, app);
|
||||||
|
|
||||||
|
let user_count = user_manager.count().await?;
|
||||||
|
if user_count == 0 {
|
||||||
|
return Ok(AdminRights { auth: None });
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth = auth_future.await?;
|
||||||
|
if user_manager.is_admin(&auth.username).await? {
|
||||||
|
Ok(AdminRights { auth: Some(auth) })
|
||||||
|
} else {
|
||||||
|
Err(APIError::AdminPermissionRequired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
src/server/axum/error.rs
Normal file
50
src/server/axum/error.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
|
use crate::server::error::APIError;
|
||||||
|
|
||||||
|
impl IntoResponse for APIError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let message = self.to_string();
|
||||||
|
let status_code = match self {
|
||||||
|
APIError::AuthorizationTokenEncoding => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::AdminPermissionRequired => StatusCode::UNAUTHORIZED,
|
||||||
|
APIError::AudioFileIOError => StatusCode::NOT_FOUND,
|
||||||
|
APIError::AuthenticationRequired => StatusCode::UNAUTHORIZED,
|
||||||
|
APIError::BrancaTokenEncoding => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::DdnsUpdateQueryFailed(s) => {
|
||||||
|
StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
||||||
|
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
||||||
|
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
|
||||||
|
APIError::EmptyUsername => StatusCode::BAD_REQUEST,
|
||||||
|
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
|
||||||
|
APIError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::Io(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::LastFMAccountNotLinked => StatusCode::NO_CONTENT,
|
||||||
|
APIError::LastFMLinkContentBase64DecodeError => StatusCode::BAD_REQUEST,
|
||||||
|
APIError::LastFMLinkContentEncodingError => StatusCode::BAD_REQUEST,
|
||||||
|
APIError::LastFMNowPlaying(_) => StatusCode::FAILED_DEPENDENCY,
|
||||||
|
APIError::LastFMScrobble(_) => StatusCode::FAILED_DEPENDENCY,
|
||||||
|
APIError::LastFMScrobblerAuthentication(_) => StatusCode::FAILED_DEPENDENCY,
|
||||||
|
APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT,
|
||||||
|
APIError::PasswordHashing => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::PlaylistNotFound => StatusCode::NOT_FOUND,
|
||||||
|
APIError::Settings(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::SongMetadataNotFound => StatusCode::NOT_FOUND,
|
||||||
|
APIError::ThumbnailFlacDecoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND,
|
||||||
|
APIError::ThumbnailId3Decoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::ThumbnailImageDecoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::ThumbnailMp4Decoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::TomlDeserialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::UnsupportedThumbnailFormat(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
APIError::UserNotFound => StatusCode::NOT_FOUND,
|
||||||
|
APIError::VFSPathNotFound => StatusCode::NOT_FOUND,
|
||||||
|
};
|
||||||
|
|
||||||
|
(status_code, message).into_response()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue