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