//! Release management API endpoints (admin only) use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use crate::auth::AdminUser; use crate::db; use crate::AppState; use super::auth::ErrorResponse; /// Release info response #[derive(Debug, Serialize)] pub struct ReleaseInfo { pub id: String, pub version: String, pub download_url: String, pub checksum_sha256: String, pub release_notes: Option, pub is_stable: bool, pub is_mandatory: bool, pub min_version: Option, pub created_at: String, } impl From for ReleaseInfo { fn from(r: db::Release) -> Self { Self { id: r.id.to_string(), version: r.version, download_url: r.download_url, checksum_sha256: r.checksum_sha256, release_notes: r.release_notes, is_stable: r.is_stable, is_mandatory: r.is_mandatory, min_version: r.min_version, created_at: r.created_at.to_rfc3339(), } } } /// Version info for unauthenticated endpoint #[derive(Debug, Serialize)] pub struct VersionInfo { pub latest_version: String, pub download_url: String, pub checksum_sha256: String, pub is_mandatory: bool, pub release_notes: Option, } /// Create release request #[derive(Debug, Deserialize)] pub struct CreateReleaseRequest { pub version: String, pub download_url: String, pub checksum_sha256: String, pub release_notes: Option, pub is_stable: bool, pub is_mandatory: bool, pub min_version: Option, } /// Update release request #[derive(Debug, Deserialize)] pub struct UpdateReleaseRequest { pub release_notes: Option, pub is_stable: bool, pub is_mandatory: bool, } /// GET /api/version - Get latest version info (no auth required) pub async fn get_version( State(state): State, ) -> Result, (StatusCode, Json)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; let release = db::get_latest_stable_release(db.pool()) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to fetch version".to_string(), }), ) })? .ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "No stable release available".to_string(), }), ) })?; Ok(Json(VersionInfo { latest_version: release.version, download_url: release.download_url, checksum_sha256: release.checksum_sha256, is_mandatory: release.is_mandatory, release_notes: release.release_notes, })) } /// GET /api/releases - List all releases (admin only) pub async fn list_releases( State(state): State, _admin: AdminUser, ) -> Result>, (StatusCode, Json)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; let releases = db::get_all_releases(db.pool()) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to fetch releases".to_string(), }), ) })?; Ok(Json(releases.into_iter().map(ReleaseInfo::from).collect())) } /// POST /api/releases - Create new release (admin only) pub async fn create_release( State(state): State, _admin: AdminUser, Json(request): Json, ) -> Result, (StatusCode, Json)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; // Validate version format (basic check) if request.version.is_empty() { return Err(( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Version cannot be empty".to_string(), }), )); } // Validate checksum format (64 hex chars for SHA-256) if request.checksum_sha256.len() != 64 || !request.checksum_sha256.chars().all(|c| c.is_ascii_hexdigit()) { return Err(( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Invalid SHA-256 checksum format (expected 64 hex characters)".to_string(), }), )); } // Validate URL if !request.download_url.starts_with("https://") { return Err(( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Download URL must use HTTPS".to_string(), }), )); } // Check if version already exists if db::get_release_by_version(db.pool(), &request.version) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Database error".to_string(), }), ) })? .is_some() { return Err(( StatusCode::CONFLICT, Json(ErrorResponse { error: "Version already exists".to_string(), }), )); } let release = db::create_release( db.pool(), &request.version, &request.download_url, &request.checksum_sha256, request.release_notes.as_deref(), request.is_stable, request.is_mandatory, request.min_version.as_deref(), ) .await .map_err(|e| { tracing::error!("Failed to create release: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to create release".to_string(), }), ) })?; tracing::info!( "Created release: {} (stable={}, mandatory={})", release.version, release.is_stable, release.is_mandatory ); Ok(Json(ReleaseInfo::from(release))) } /// GET /api/releases/:version - Get release by version (admin only) pub async fn get_release( State(state): State, _admin: AdminUser, Path(version): Path, ) -> Result, (StatusCode, Json)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; let release = db::get_release_by_version(db.pool(), &version) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Database error".to_string(), }), ) })? .ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Release not found".to_string(), }), ) })?; Ok(Json(ReleaseInfo::from(release))) } /// PUT /api/releases/:version - Update release (admin only) pub async fn update_release( State(state): State, _admin: AdminUser, Path(version): Path, Json(request): Json, ) -> Result, (StatusCode, Json)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; let release = db::update_release( db.pool(), &version, request.release_notes.as_deref(), request.is_stable, request.is_mandatory, ) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to update release".to_string(), }), ) })? .ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Release not found".to_string(), }), ) })?; tracing::info!( "Updated release: {} (stable={}, mandatory={})", release.version, release.is_stable, release.is_mandatory ); Ok(Json(ReleaseInfo::from(release))) } /// DELETE /api/releases/:version - Delete release (admin only) pub async fn delete_release( State(state): State, _admin: AdminUser, Path(version): Path, ) -> Result)> { let db = state.db.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ErrorResponse { error: "Database not available".to_string(), }), ) })?; let deleted = db::delete_release(db.pool(), &version) .await .map_err(|e| { tracing::error!("Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to delete release".to_string(), }), ) })?; if deleted { tracing::info!("Deleted release: {}", version); Ok(StatusCode::NO_CONTENT) } else { Err(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Release not found".to_string(), }), )) } }