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