- Cargo workspace with hiy-server (axum 0.7 + sqlx SQLite + tokio) - SQLite schema: apps, deploys, env_vars (inline migrations, no daemon) - Background build worker: sequential queue, streams stdout/stderr to DB - REST API: CRUD for apps, deploys, env vars; GitHub webhook with HMAC-SHA256 - SSE endpoint for live build log streaming - Monospace HTMX-free dashboard: app list + per-app detail, log viewer, env editor - builder/build.sh: clone/pull → detect strategy (Dockerfile/buildpack/static) → docker build → swap container → update Caddy via admin API → prune images - infra/docker-compose.yml + Dockerfile.server for local dev (no Pi needed) - proxy/Caddyfile: auto-HTTPS off for local, comment removed for production - .env.example Compiles clean (zero warnings). Run locally: cp .env.example .env && cargo run --bin hiy-server https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
113 lines
3.3 KiB
Rust
113 lines
3.3 KiB
Rust
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<AppState>) -> Result<Json<Vec<App>>, 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<AppState>,
|
|
Json(payload): Json<CreateApp>,
|
|
) -> Result<(StatusCode, Json<App>), 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<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<App>, StatusCode> {
|
|
fetch_app(&s, &id).await.map(Json)
|
|
}
|
|
|
|
pub async fn update(
|
|
State(s): State<AppState>,
|
|
Path(id): Path<String>,
|
|
Json(payload): Json<UpdateApp>,
|
|
) -> Result<Json<App>, 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<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<StatusCode, StatusCode> {
|
|
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<App, StatusCode> {
|
|
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)
|
|
}
|