Files
narlblog/backend/src/main.rs
T

121 lines
4.0 KiB
Rust

pub mod auth;
pub mod error;
pub mod handlers;
pub mod models;
use axum::{
Router,
extract::DefaultBodyLimit,
http::{HeaderValue, header},
routing::{delete, get, post},
};
use std::{env, fs, path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use tower_http::{
cors::{AllowOrigin, CorsLayer},
services::ServeDir,
};
use tracing::{error, info, warn};
pub struct AppState {
pub admin_token: String,
pub data_dir: PathBuf,
pub cookie_secure: bool,
pub post_lock: Mutex<()>,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
dotenvy::dotenv().ok();
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
let admin_token = env::var("ADMIN_TOKEN")
.ok()
.filter(|t| !t.trim().is_empty())
.expect("ADMIN_TOKEN must be set to a non-empty value");
if admin_token.len() < 16 {
warn!(
"ADMIN_TOKEN is shorter than 16 characters. Use a long random string in production."
);
}
let data_dir_str = env::var("DATA_DIR").unwrap_or_else(|_| "../data".to_string());
let data_dir = PathBuf::from(data_dir_str);
let cookie_secure = env::var("COOKIE_SECURE")
.map(|v| v != "false" && v != "0")
.unwrap_or(true);
info!("Initializing backend with data dir: {:?}", data_dir);
let posts_dir = data_dir.join("posts");
let uploads_dir = data_dir.join("uploads");
if let Err(e) = fs::create_dir_all(&posts_dir) {
error!("Failed to create posts directory: {}", e);
}
if let Err(e) = fs::create_dir_all(&uploads_dir) {
error!("Failed to create uploads directory: {}", e);
}
let state = Arc::new(AppState {
admin_token,
data_dir,
cookie_secure,
post_lock: Mutex::new(()),
});
// CORS — locked down by default. Set FRONTEND_ORIGIN to the public URL of
// the frontend if you ever expose the backend directly to browsers.
// Normal deployments hit the backend through the Astro proxy, which is
// server-to-server and not subject to CORS.
let cors = match env::var("FRONTEND_ORIGIN").ok().filter(|s| !s.is_empty()) {
Some(origin) => {
let value = HeaderValue::from_str(&origin)
.expect("FRONTEND_ORIGIN must be a valid origin URL");
CorsLayer::new()
.allow_origin(AllowOrigin::exact(value))
.allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::DELETE,
axum::http::Method::OPTIONS,
])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
.allow_credentials(true)
}
None => CorsLayer::new(),
};
let app = Router::new()
.route("/api/auth/login", post(handlers::auth::login))
.route("/api/auth/logout", post(handlers::auth::logout))
.route("/api/auth/me", get(handlers::auth::me))
.route(
"/api/config",
get(handlers::config::get_config).post(handlers::config::update_config),
)
.route(
"/api/posts",
get(handlers::posts::list_posts).post(handlers::posts::create_post),
)
.route(
"/api/posts/{slug}",
get(handlers::posts::get_post).delete(handlers::posts::delete_post),
)
.route("/api/uploads", get(handlers::upload::list_uploads))
.route(
"/api/uploads/{filename}",
delete(handlers::upload::delete_upload),
)
.route("/api/upload", post(handlers::upload::upload_file))
.route("/healthz", get(|| async { "ok" }))
.nest_service("/uploads", ServeDir::new(uploads_dir))
.layer(DefaultBodyLimit::max(50 * 1024 * 1024))
.layer(cors)
.with_state(state);
let addr = format!("0.0.0.0:{}", port);
info!("Starting server on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}