setup local dev env
This commit is contained in:
124
backend/content-service/src/main.rs
Normal file
124
backend/content-service/src/main.rs
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user