iqps_backend/routing/
mod.rs

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