Skip to main content

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        .route("/stats", axum::routing::get(handlers::get_stats))
59        .layer(DefaultBodyLimit::max(2 << 20)) // Default limit of 2 MiB
60        .route("/upload", axum::routing::post(handlers::upload))
61        .layer(DefaultBodyLimit::max(50 << 20)) // 50 MiB limit for upload endpoint
62        .with_state(state)
63        .layer(
64            TraceLayer::new_for_http()
65                .make_span_with(trace::DefaultMakeSpan::new().level(tracing::Level::INFO))
66                .on_response(trace::DefaultOnResponse::new().level(tracing::Level::INFO)),
67        )
68        .layer(
69            CorsLayer::new()
70                .allow_headers(Any)
71                .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
72                .allow_origin(cors_origins),
73        )
74}
75
76/// The state of the axum router, containing the environment variables and the database connection.
77struct RouterState {
78    pub db: db::Database,
79    pub env_vars: EnvVars,
80}
81type HandlerState = State<Arc<RouterState>>;
82
83/// Standard backend response format (serialized as JSON)
84#[derive(serde::Serialize)]
85struct BackendResponse<T: Serialize> {
86    /// Whether the operation succeeded or failed
87    pub status: &'static str,
88    /// A message describing the state of the operation (success/failure message)
89    pub message: String,
90    /// Any optional data sent (only sent if the operation was a success)
91    pub data: Option<T>,
92}
93
94impl<T: serde::Serialize> BackendResponse<T> {
95    /// Creates a new success backend response with the given message and data
96    pub fn ok(message: String, data: T) -> (StatusCode, Self) {
97        (
98            StatusCode::OK,
99            Self {
100                status: "success",
101                message,
102                data: Some(data),
103            },
104        )
105    }
106
107    /// Creates a new error backend response with the given message, data, and an HTTP status code
108    pub fn error(message: String, status_code: StatusCode) -> (StatusCode, Self) {
109        (
110            status_code,
111            Self {
112                status: "error",
113                message,
114                data: None,
115            },
116        )
117    }
118}
119
120impl<T: Serialize> IntoResponse for BackendResponse<T> {
121    fn into_response(self) -> axum::response::Response {
122        Json(self).into_response()
123    }
124}
125
126/// 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.
127pub(super) struct AppError(color_eyre::eyre::Error);
128impl IntoResponse for AppError {
129    fn into_response(self) -> axum::response::Response {
130        tracing::error!("An error occured: {}", self.0);
131
132        BackendResponse::<()>::error(
133            "An internal server error occured. Please try again later.".into(),
134            StatusCode::INTERNAL_SERVER_ERROR,
135        )
136        .into_response()
137    }
138}
139
140impl<E> From<E> for AppError
141where
142    E: Into<color_eyre::eyre::Error>,
143{
144    fn from(err: E) -> Self {
145        Self(err.into())
146    }
147}