crates/server: REST API routes; RBAC auth middleware; cookie sessions; dashboard

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I5298a925bd9c11780e49d8b1c98eebd86a6a6964
This commit is contained in:
raf 2026-02-01 15:13:33 +03:00
commit 235d3d38a6
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
38 changed files with 6275 additions and 7 deletions

View file

@ -0,0 +1,126 @@
use axum::response::sse::{Event, KeepAlive};
use axum::{
Router,
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response, Sse},
routing::get,
};
use uuid::Uuid;
use crate::error::ApiError;
use crate::state::AppState;
async fn get_build_log(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Response, ApiError> {
// Verify build exists
let _build = fc_common::repo::builds::get(&state.pool, id)
.await
.map_err(ApiError)?;
let log_storage = fc_common::log_storage::LogStorage::new(state.config.logs.log_dir.clone())
.map_err(|e| ApiError(fc_common::CiError::Io(e)))?;
match log_storage.read_log(&id) {
Ok(Some(content)) => Ok((
StatusCode::OK,
[("content-type", "text/plain; charset=utf-8")],
content,
)
.into_response()),
Ok(None) => Ok((StatusCode::NOT_FOUND, "No log available for this build").into_response()),
Err(e) => Err(ApiError(fc_common::CiError::Io(e))),
}
}
async fn stream_build_log(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>>, ApiError> {
let build = fc_common::repo::builds::get(&state.pool, id)
.await
.map_err(ApiError)?;
let log_storage = fc_common::log_storage::LogStorage::new(state.config.logs.log_dir.clone())
.map_err(|e| ApiError(fc_common::CiError::Io(e)))?;
let active_path = log_storage.log_path_for_active(&id);
let final_path = log_storage.log_path(&id);
let pool = state.pool.clone();
let build_id = build.id;
let stream = async_stream::stream! {
use tokio::io::{AsyncBufReadExt, BufReader};
// Determine which file to read
let path = if active_path.exists() {
active_path.clone()
} else if final_path.exists() {
final_path.clone()
} else {
// Wait for the file to appear
let mut found = false;
for _ in 0..30 {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
if active_path.exists() || final_path.exists() {
found = true;
break;
}
}
if !found {
yield Ok(Event::default().data("No log file available"));
return;
}
if active_path.exists() { active_path.clone() } else { final_path.clone() }
};
let file = match tokio::fs::File::open(&path).await {
Ok(f) => f,
Err(_) => {
yield Ok(Event::default().data("Failed to open log file"));
return;
}
};
let mut reader = BufReader::new(file);
let mut line = String::new();
let mut consecutive_empty = 0u32;
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => {
// EOF — check if build is still running
consecutive_empty += 1;
if consecutive_empty > 5 {
// Check build status
if let Ok(b) = fc_common::repo::builds::get(&pool, build_id).await {
if b.status != fc_common::models::BuildStatus::Running
&& b.status != fc_common::models::BuildStatus::Pending {
yield Ok(Event::default().event("done").data("Build completed"));
return;
}
}
consecutive_empty = 0;
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
Ok(_) => {
consecutive_empty = 0;
yield Ok(Event::default().data(line.trim_end()));
}
Err(_) => return,
}
}
};
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
}
pub fn router() -> Router<AppState> {
Router::new()
.route("/builds/{id}/log", get(get_build_log))
.route("/builds/{id}/log/stream", get(stream_build_log))
}