1use authbeam::api::profile::read_image;
2use carp::{Graph, CarpGraph};
3use crate::database::Database;
4use axum::{
5 body::{Body, Bytes},
6 extract::{Query, State},
7 http::HeaderMap,
8 response::IntoResponse,
9 routing::{get, post},
10 Json, Router,
11};
12use serde::{Deserialize, Serialize};
13use pathbufd::pathd;
14
15pub fn routes(database: Database) -> Router {
16 Router::new()
17 .route("/lang", get(langfile_request))
18 .route("/lang/set", post(set_langfile_request))
19 .route("/ext/image", get(external_image_request))
20 .route("/carpsvg", post(render_carpgraph))
21 .with_state(database.clone())
23}
24
25#[derive(Serialize, Deserialize)]
26pub struct ExternalImageQuery {
27 pub img: String,
28}
29
30pub async fn external_image_request(
32 Query(props): Query<ExternalImageQuery>,
33 State(database): State<Database>,
34) -> impl IntoResponse {
35 let image_url = &props.img;
36
37 if image_url.starts_with(&database.config.host) {
38 return (
39 [("Content-Type", "image/svg+xml")],
40 Body::from(read_image(
41 pathd!("{}/images", database.config.static_dir),
42 "default-banner.svg".to_string(),
43 )),
44 );
45 }
46
47 for host in database.config.blocked_hosts {
48 if image_url.starts_with(&host) {
49 return (
50 [("Content-Type", "image/svg+xml")],
51 Body::from(read_image(
52 pathd!("{}/images", database.config.static_dir),
53 "default-banner.svg".to_string(),
54 )),
55 );
56 }
57 }
58
59 if image_url.is_empty() {
61 return (
62 [("Content-Type", "image/svg+xml")],
63 Body::from(read_image(
64 pathd!("{}/images", database.config.static_dir),
65 "default-banner.svg".to_string(),
66 )),
67 );
68 }
69
70 let guessed_mime = mime_guess::from_path(image_url)
71 .first_raw()
72 .unwrap_or("application/octet-stream");
73
74 match database.auth.http.get(image_url).send().await {
75 Ok(stream) => {
76 let size = stream.content_length();
77 if size.unwrap_or_default() > 10485760 {
78 return (
80 [("Content-Type", "image/svg+xml")],
81 Body::from(read_image(
82 pathd!("{}/images", database.config.static_dir),
83 "default-banner.svg".to_string(),
84 )),
85 );
86 }
87
88 if let Some(ct) = stream.headers().get("Content-Type") {
89 let ct = ct.to_str().unwrap();
90 let bad_ct = vec!["text/html", "text/plain"];
91 if (!ct.starts_with("image/") && !ct.starts_with("font/")) | bad_ct.contains(&ct) {
92 return (
94 [("Content-Type", "image/svg+xml")],
95 Body::from(read_image(
96 pathd!("{}/images", database.config.static_dir),
97 "default-banner.svg".to_string(),
98 )),
99 );
100 }
101 }
102
103 (
104 [(
105 "Content-Type",
106 if guessed_mime == "text/html" {
107 "text/plain"
108 } else {
109 guessed_mime
110 },
111 )],
112 Body::from_stream(stream.bytes_stream()),
113 )
114 }
115 Err(_) => (
116 [("Content-Type", "image/svg+xml")],
117 Body::from(read_image(
118 pathd!("{}/images", database.config.static_dir),
119 "default-banner.svg".to_string(),
120 )),
121 ),
122 }
123}
124
125#[derive(Serialize, Deserialize)]
126pub struct LangFileQuery {
127 #[serde(default)]
128 pub id: String,
129}
130
131pub async fn langfile_request(
133 Query(props): Query<LangFileQuery>,
134 State(database): State<Database>,
135) -> impl IntoResponse {
136 Json(database.lang(&props.id))
137}
138
139pub async fn set_langfile_request(Query(props): Query<LangFileQuery>) -> impl IntoResponse {
141 (
142 {
143 let mut headers = HeaderMap::new();
144
145 headers.insert(
146 "Set-Cookie",
147 format!(
148 "net.rainbeam.langs.choice={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
149 props.id,
150 60* 60 * 24 * 365
151 )
152 .parse()
153 .unwrap(),
154 );
155
156 headers
157 },
158 "Language changed",
159 )
160}
161
162pub async fn render_carpgraph(data: Bytes) -> impl IntoResponse {
163 Graph::from_bytes(data.to_vec()).to_svg()
164}