setup local dev env

This commit is contained in:
2026-03-10 17:13:23 +01:00
parent b27db93a90
commit f4d3fade9b
61 changed files with 6695 additions and 228 deletions

View File

@@ -0,0 +1,124 @@
use axum::{
extract::State,
http::{header::AUTHORIZATION, HeaderMap, StatusCode},
response::IntoResponse,
routing::{any, get},
Json, Router,
};
use common::{decode_jwt, AppError, AppResult};
use serde::Serialize;
use sqlx::{postgres::PgPoolOptions, PgPool};
use tower_http::cors::CorsLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Clone)]
struct AppState {
content_pool: PgPool,
jwt_secret: String,
}
#[derive(Serialize)]
struct HealthResponse {
service: &'static str,
status: &'static str,
}
#[derive(Serialize)]
struct RulesetSummary {
id: &'static str,
name: &'static str,
}
#[tokio::main]
async fn main() {
let _ = dotenvy::dotenv();
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "content_service=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let content_database_url =
std::env::var("CONTENT_DATABASE_URL").expect("CONTENT_DATABASE_URL must be set");
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let port: u16 = std::env::var("CONTENT_PORT")
.or_else(|_| std::env::var("PORT"))
.unwrap_or_else(|_| "3001".to_string())
.parse()
.expect("PORT must be a valid u16");
let content_pool = PgPoolOptions::new()
.max_connections(10)
.connect_lazy(&content_database_url)
.expect("invalid CONTENT_DATABASE_URL");
let state = AppState {
content_pool,
jwt_secret,
};
let app = Router::new()
.route("/health", get(health))
.route("/v1/rulesets", get(list_rulesets))
.route("/v1/*path", any(not_implemented))
.with_state(state)
.layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
.await
.expect("failed to bind");
tracing::info!(
"content-service listening on {}",
listener.local_addr().unwrap()
);
axum::serve(listener, app).await.expect("server error");
}
async fn health(State(state): State<AppState>) -> impl IntoResponse {
let _ = &state.content_pool;
Json(HealthResponse {
service: "content-service",
status: "ok",
})
}
async fn list_rulesets(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Vec<RulesetSummary>>> {
require_bearer(&state.jwt_secret, &headers)?;
Ok(Json(vec![
RulesetSummary {
id: "generic",
name: "Generic",
},
RulesetSummary {
id: "dsa5e",
name: "DSA5e",
},
]))
}
fn require_bearer(jwt_secret: &str, headers: &HeaderMap) -> AppResult<()> {
let header = headers
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| AppError::Unauthorized("missing Authorization header".to_string()))?;
let token = header
.strip_prefix("Bearer ")
.ok_or_else(|| AppError::Unauthorized("expected Bearer token".to_string()))?;
let _claims = decode_jwt(jwt_secret, token)?;
Ok(())
}
async fn not_implemented() -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, "not implemented")
}