iqps_backend/routing/
mod.rs1use 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
27pub 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)) .route("/upload", axum::routing::post(handlers::upload))
61 .layer(DefaultBodyLimit::max(50 << 20)) .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
76struct RouterState {
78 pub db: db::Database,
79 pub env_vars: EnvVars,
80}
81type HandlerState = State<Arc<RouterState>>;
82
83#[derive(serde::Serialize)]
85struct BackendResponse<T: Serialize> {
86 pub status: &'static str,
88 pub message: String,
90 pub data: Option<T>,
92}
93
94impl<T: serde::Serialize> BackendResponse<T> {
95 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 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
126pub(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}