Adds settings endpoints

This commit is contained in:
Antoine Gersant 2024-07-13 14:12:54 -07:00
parent 84921f7db3
commit 5c4631c673
5 changed files with 232 additions and 20 deletions

View file

@ -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]

View file

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

View file

@ -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
View 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
View 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()
}
}