rb/routing/api/
util.rs

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        // ...
22        .with_state(database.clone())
23}
24
25#[derive(Serialize, Deserialize)]
26pub struct ExternalImageQuery {
27    pub img: String,
28}
29
30/// Proxy an external image
31pub 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    // get profile image
60    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 defualt image (content too big)
79                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                    // if we got html, return default banner (likely an error page)
93                    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
131/// Get a langfile
132pub 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
139/// Set a langfile
140pub 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}