Hostityourself/server/src/routes/apps.rs
Claude 8f5bb158cb
M1: Rust control plane, builder, dashboard, and infra
- 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
2026-03-19 08:25:59 +00:00

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