iqps_backend/routing/
mod.rs

1//! Router, [`handlers`], [`middleware`], state, and response utils.
2
3use std::sync::Arc;
4
5use axum::{
6    extract::{DefaultBodyLimit, Json, State},
7    http::StatusCode,
8    response::IntoResponse,
9};
10use http::{HeaderValue, Method};
11use serde::Serialize;
12use tower_http::{
13    cors::{Any, CorsLayer},
14    trace::{self, TraceLayer},
15};
16
17use crate::{
18    db::{self, Database},
19    env::EnvVars,
20};
21
22mod handlers;
23mod middleware;
24
25pub use handlers::{EditReq, FileDetails};
26
27/// Returns the Axum router for IQPS
28pub fn get_router(env_vars: EnvVars, db: Database) -> axum::Router {
29    let cors_origins = env_vars
30        .cors_allowed_origins
31        .split(',')
32        .map(|origin| {
33            origin
34                .trim()
35                .parse::<HeaderValue>()
36                .expect("CORS Allowed Origins Invalid")
37        })
38        .collect::<Vec<HeaderValue>>();
39
40    let state = Arc::new(RouterState { db, env_vars });
41
42    axum::Router::new()
43        .route("/unapproved", axum::routing::get(handlers::get_unapproved))
44        .route("/trash", axum::routing::get(handlers::get_trash))
45        .route("/details", axum::routing::get(handlers::get_paper_details))
46        .route("/profile", axum::routing::get(handlers::profile))
47        .route("/edit", axum::routing::post(handlers::edit))
48        .route("/delete", axum::routing::post(handlers::delete))
49        .route("/harddelete", axum::routing::post(handlers::hard_delete))
50        .route("/similar", axum::routing::get(handlers::similar))
51        .route_layer(axum::middleware::from_fn_with_state(
52            state.clone(),
53            middleware::verify_jwt_middleware,
54        ))
55        .route("/oauth", axum::routing::post(handlers::oauth))
56        .route("/healthcheck", axum::routing::get(handlers::healthcheck))
57        .route("/search", axum::routing::get(handlers::search))
58        .layer(DefaultBodyLimit::max(2 << 20)) // Default limit of 2 MiB
59        .route("/upload", axum::routing::post(handlers::upload))
60        .layer(DefaultBodyLimit::max(50 << 20)) // 50 MiB limit for upload endpoint
61        .with_state(state)
62        .layer(
63            TraceLayer::new_for_http()
64                .make_span_with(trace::DefaultMakeSpan::new().level(tracing::Level::INFO))
65                .on_response(trace::DefaultOnResponse::new().level(tracing::Level::INFO)),
66        )
67        .layer(
68            CorsLayer::new()
69                .allow_headers(Any)
70                .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
71                .allow_origin(cors_origins),
72        )
73}
74
75/// The state of the axum router, containing the environment variables and the database connection.
76struct RouterState {
77    pub db: db::Database,
78    pub env_vars: EnvVars,
79}
80type HandlerState = State<Arc<RouterState>>;
81
82/// Standard backend response format (serialized as JSON)
83#[derive(serde::Serialize)]
84struct BackendResponse<T: Serialize> {
85    /// Whether the operation succeeded or failed
86    pub status: &'static str,
87    /// A message describing the state of the operation (success/failure message)
88    pub message: String,
89    /// Any optional data sent (only sent if the operation was a success)
90    pub data: Option<T>,
91}
92
93impl<T: serde::Serialize> BackendResponse<T> {
94    /// Creates a new success backend response with the given message and data
95    pub fn ok(message: String, data: T) -> (StatusCode, Self) {
96        (
97            StatusCode::OK,
98            Self {
99                status: "success",
100                message,
101                data: Some(data),
102            },
103        )
104    }
105
106    /// Creates a new error backend response with the given message, data, and an HTTP status code
107    pub fn error(message: String, status_code: StatusCode) -> (StatusCode, Self) {
108        (
109            status_code,
110            Self {
111                status: "error",
112                message,
113                data: None,
114            },
115        )
116    }
117}
118
119impl<T: Serialize> IntoResponse for BackendResponse<T> {
120    fn into_response(self) -> axum::response::Response {
121        Json(self).into_response()
122    }
123}
124
125/// A struct representing the error returned by a handler. This is automatically serialized into JSON and sent as an internal server error (500) backend response. The `?` operator can be used anywhere inside a handler to do so.
126pub(super) struct AppError(color_eyre::eyre::Error);
127impl IntoResponse for AppError {
128    fn into_response(self) -> axum::response::Response {
129        tracing::error!("An error occured: {}", self.0);
130
131        BackendResponse::<()>::error(
132            "An internal server error occured. Please try again later.".into(),
133            StatusCode::INTERNAL_SERVER_ERROR,
134        )
135        .into_response()
136    }
137}
138
139impl<E> From<E> for AppError
140where
141    E: Into<color_eyre::eyre::Error>,
142{
143    fn from(err: E) -> Self {
144        Self(err.into())
145    }
146}