Files
xevion.dev/src/routes.rs

168 lines
5.9 KiB
Rust

use axum::{
Router,
body::Body,
extract::Request,
http::{Method, Uri},
response::IntoResponse,
routing::{any, delete, get, post, put},
};
use std::sync::Arc;
use crate::{assets, handlers, state::AppState};
/// Build API routes
pub fn api_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/", any(api_root_404_handler))
.route(
"/health",
get(handlers::health_handler).head(handlers::health_handler),
)
// Authentication endpoints (public)
.route("/login", post(handlers::api_login_handler))
.route("/logout", post(handlers::api_logout_handler))
.route("/session", get(handlers::api_session_handler))
// Projects - GET is public (shows all for admin, only non-hidden for public)
// POST/PUT/DELETE require authentication
// {ref} accepts either UUID or slug
.route(
"/projects",
get(handlers::projects_handler).post(handlers::create_project_handler),
)
.route(
"/projects/{ref}",
get(handlers::get_project_handler)
.put(handlers::update_project_handler)
.delete(handlers::delete_project_handler),
)
// Project tags - authentication checked in handlers
.route(
"/projects/{ref}/tags",
get(handlers::get_project_tags_handler).post(handlers::add_project_tag_handler),
)
.route(
"/projects/{ref}/tags/{tag_ref}",
delete(handlers::remove_project_tag_handler),
)
// Project media - GET is public, POST/PUT/DELETE require authentication
.route(
"/projects/{ref}/media",
get(handlers::get_project_media_handler).post(handlers::upload_media_handler),
)
.route(
"/projects/{ref}/media/reorder",
put(handlers::reorder_media_handler),
)
.route(
"/projects/{ref}/media/{media_id}",
delete(handlers::delete_media_handler),
)
// Tags - authentication checked in handlers
// {ref} accepts either UUID or slug
.route(
"/tags",
get(handlers::list_tags_handler).post(handlers::create_tag_handler),
)
.route(
"/tags/{ref}",
get(handlers::get_tag_handler)
.put(handlers::update_tag_handler)
.delete(handlers::delete_tag_handler),
)
.route(
"/tags/{ref}/related",
get(handlers::get_related_tags_handler),
)
.route(
"/tags/recalculate-cooccurrence",
post(handlers::recalculate_cooccurrence_handler),
)
// Admin stats - requires authentication
.route("/stats", get(handlers::get_admin_stats_handler))
// Site settings - GET is public, PUT requires authentication
.route(
"/settings",
get(handlers::get_settings_handler).put(handlers::update_settings_handler),
)
// Icon API - proxy to SvelteKit (authentication handled by SvelteKit)
.route("/icons/{*path}", get(handlers::proxy_icons_handler))
.fallback(api_404_and_method_handler)
}
/// Build base router (shared routes for all listen addresses)
pub fn build_base_router() -> Router<Arc<AppState>> {
Router::new()
.nest("/api", api_routes())
.route("/api/", any(api_root_404_handler))
// Serve env.js explicitly before the wildcard (it's at build root, not in client/)
.route("/_app/env.js", get(handlers::serve_env_js))
.route(
"/_app/{*path}",
get(assets::serve_embedded_asset).head(assets::serve_embedded_asset),
)
.route("/pgp", get(handlers::handle_pgp_route))
.route("/publickey.asc", get(handlers::serve_pgp_key))
.route("/pgp.asc", get(handlers::serve_pgp_key))
.route("/.well-known/pgpkey.asc", get(handlers::serve_pgp_key))
.route("/keys", get(handlers::redirect_to_pgp))
}
async fn api_root_404_handler(uri: Uri) -> impl IntoResponse {
api_404_handler(uri).await
}
async fn api_404_and_method_handler(req: Request) -> impl IntoResponse {
use axum::{Json, http::StatusCode};
let method = req.method();
let uri = req.uri();
let path = uri.path();
if method != Method::GET && method != Method::HEAD && method != Method::OPTIONS {
let content_type = req
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok());
if let Some(ct) = content_type {
if !ct.starts_with("application/json") {
return (
StatusCode::UNSUPPORTED_MEDIA_TYPE,
Json(serde_json::json!({
"error": "Unsupported media type",
"message": "API endpoints only accept application/json"
})),
)
.into_response();
}
} else if method == Method::POST || method == Method::PUT || method == Method::PATCH {
// POST/PUT/PATCH require Content-Type header
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "Missing Content-Type header",
"message": "Content-Type: application/json is required"
})),
)
.into_response();
}
}
// Route not found
tracing::warn!(path = %path, method = %method, "API route not found");
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Not found",
"path": path
})),
)
.into_response()
}
async fn api_404_handler(uri: Uri) -> impl IntoResponse {
let req = Request::builder().uri(uri).body(Body::empty()).unwrap();
api_404_and_method_handler(req).await
}