use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use chrono::Utc; use uuid::Uuid; use crate::{ models::{App, CreateApp, UpdateApp}, AppState, }; pub async fn list(State(s): State) -> Result>, StatusCode> { let apps = sqlx::query_as::<_, App>("SELECT * FROM apps ORDER BY created_at DESC") .fetch_all(&s.db) .await .map_err(|e| { tracing::error!("list apps: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; Ok(Json(apps)) } pub async fn create( State(s): State, Json(payload): Json, ) -> Result<(StatusCode, Json), StatusCode> { // Use the name as the slug/id (must be URL-safe). let id = payload.name.to_lowercase().replace(' ', "-"); let now = Utc::now().to_rfc3339(); let branch = payload.branch.unwrap_or_else(|| "main".into()); let secret = Uuid::new_v4().to_string().replace('-', ""); sqlx::query( "INSERT INTO apps (id, name, repo_url, branch, port, webhook_secret, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&id) .bind(&payload.name) .bind(&payload.repo_url) .bind(&branch) .bind(payload.port) .bind(&secret) .bind(&now) .bind(&now) .execute(&s.db) .await .map_err(|e| { tracing::error!("create app: {}", e); StatusCode::UNPROCESSABLE_ENTITY })?; fetch_app(&s, &id).await.map(|a| (StatusCode::CREATED, Json(a))) } pub async fn get_one( State(s): State, Path(id): Path, ) -> Result, StatusCode> { fetch_app(&s, &id).await.map(Json) } pub async fn update( State(s): State, Path(id): Path, Json(payload): Json, ) -> Result, StatusCode> { let now = Utc::now().to_rfc3339(); if let Some(v) = payload.repo_url { sqlx::query("UPDATE apps SET repo_url = ?, updated_at = ? WHERE id = ?") .bind(v).bind(&now).bind(&id) .execute(&s.db).await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } if let Some(v) = payload.branch { sqlx::query("UPDATE apps SET branch = ?, updated_at = ? WHERE id = ?") .bind(v).bind(&now).bind(&id) .execute(&s.db).await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } if let Some(v) = payload.port { sqlx::query("UPDATE apps SET port = ?, updated_at = ? WHERE id = ?") .bind(v).bind(&now).bind(&id) .execute(&s.db).await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } fetch_app(&s, &id).await.map(Json) } pub async fn delete( State(s): State, Path(id): Path, ) -> Result { let res = sqlx::query("DELETE FROM apps WHERE id = ?") .bind(&id) .execute(&s.db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if res.rows_affected() == 0 { return Err(StatusCode::NOT_FOUND); } Ok(StatusCode::NO_CONTENT) } async fn fetch_app(s: &AppState, id: &str) -> Result { sqlx::query_as::<_, App>("SELECT * FROM apps WHERE id = ?") .bind(id) .fetch_optional(&s.db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND) }