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"
|
||||
futures-util = { version = "0.3" }
|
||||
getopts = "0.2.21"
|
||||
headers = "0.4"
|
||||
http = "1.1.0"
|
||||
id3 = "1.7.0"
|
||||
lewton = "0.10.2"
|
||||
log = "0.4.17"
|
||||
|
@ -76,8 +78,6 @@ winres = "0.1"
|
|||
[dev-dependencies]
|
||||
axum-test = "15.3"
|
||||
bytes = "1.6.1"
|
||||
headers = "0.4"
|
||||
http = "1.1.0"
|
||||
fs_extra = "1.2.0"
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
|
|
|
@ -1,33 +1,21 @@
|
|||
use axum::Router;
|
||||
use axum::{extract::FromRef, Router};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::{self, App};
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod error;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
pub fn make_router(app: App) -> Router {
|
||||
Router::new()
|
||||
.nest("/api", api::router())
|
||||
.with_state(app.clone())
|
||||
.nest_service("/swagger", ServeDir::new(app.swagger_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> {
|
||||
|
@ -39,3 +27,21 @@ pub async fn launch(app: App) -> Result<(), std::io::Error> {
|
|||
|
||||
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