use axum::Json;
use axum::extract::State;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use yorishiro_core::models::maintenance::{self, MaintenanceMode, MaintenanceState};
use crate::error::ApiError;
use crate::http::middleware::auth::{Authorized, MigrationScope};
use crate::state::AppState;
#[derive(Debug, Serialize, ToSchema)]
pub struct MaintenanceResponse {
pub mode: String,
pub retry_after: u32,
pub reason: Option<String>,
}
impl From<MaintenanceState> for MaintenanceResponse {
fn from(state: MaintenanceState) -> Self {
Self {
mode: state.mode.as_db_str().to_string(),
retry_after: state.retry_after,
reason: state.reason,
}
}
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct SetMaintenanceRequest {
pub mode: String,
#[serde(default)]
pub retry_after: Option<u32>,
#[serde(default)]
pub reason: Option<String>,
}
const DEFAULT_RETRY_AFTER: u32 = 300;
pub(crate) fn parse_mode(value: &str) -> Option<MaintenanceMode> {
match value {
"off" => Some(MaintenanceMode::Off),
"read-only" | "read_only" => Some(MaintenanceMode::ReadOnly),
"full-lock" | "full_lock" => Some(MaintenanceMode::FullLock),
_ => None,
}
}
#[utoipa::path(
get,
path = "/api/system/maintenance",
responses(
(status = 200, description = "The current state", body = MaintenanceResponse),
(status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
(status = 403, description = "Insufficient scope", body = crate::error::ApiErrorBody),
),
tag = "system",
)]
pub async fn get_maintenance(
State(state): State<AppState>,
_authorized: Authorized<MigrationScope>,
) -> Result<Json<MaintenanceResponse>, ApiError> {
let mut conn = state
.identity_pool
.acquire()
.await
.map_err(|err| ApiError::from(yorishiro_core::YorishiroError::Internal(err.into())))?;
let current = maintenance::get(&mut *conn).await?;
Ok(Json(current.into()))
}
#[utoipa::path(
put,
path = "/api/system/maintenance",
request_body = SetMaintenanceRequest,
responses(
(status = 200, description = "The state as it now stands", body = MaintenanceResponse),
(status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
(status = 403, description = "Insufficient scope", body = crate::error::ApiErrorBody),
(status = 422, description = "Unknown mode", body = crate::error::ApiErrorBody),
),
tag = "system",
)]
pub async fn set_maintenance(
State(state): State<AppState>,
_authorized: Authorized<MigrationScope>,
Json(body): Json<SetMaintenanceRequest>,
) -> Result<Json<MaintenanceResponse>, ApiError> {
let mode =
parse_mode(&body.mode).ok_or_else(|| yorishiro_core::YorishiroError::ValidationFailed {
message: format!("unknown maintenance mode '{}'", body.mode),
details: vec![],
hint: "one of: off, read-only, full-lock".into(),
})?;
let updated = maintenance::set(
&state.identity_pool,
mode,
body.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
body.reason,
)
.await?;
Ok(Json(updated.into()))
}
#[cfg(test)]
#[path = "../../../tests/http/controllers/system.rs"]
mod tests;