rb/routing/api/
profiles.rs

1use crate::database::Database;
2use crate::model::{DataExportOptions, DatabaseError};
3use crate::ToHtml;
4use axum::body::Body;
5use axum::extract::Query;
6use axum::http::{HeaderMap, HeaderValue, Response};
7use axum_extra::extract::CookieJar;
8use hcaptcha_no_wasm::Hcaptcha;
9
10use authbeam::model::{FinePermission, IpBlockCreate, NotificationCreate};
11use databeam::prelude::DefaultReturn;
12
13use axum::{
14    extract::{Path, State},
15    response::{IntoResponse, Redirect},
16    routing::{get, post},
17    Json, Router,
18};
19
20pub fn routes(database: Database) -> Router {
21    Router::new()
22        .route("/{id}/report", post(report_request))
23        .route("/{id}/export", get(export_request)) // staff
24        .route("/{id}/ipblock", post(ipblock_request))
25        // ...
26        .with_state(database)
27}
28
29// routes
30
31/// Redirect an ID to a full username
32pub async fn expand_request(
33    Path(id): Path<String>,
34    State(database): State<Database>,
35) -> Response<Body> {
36    match database.get_profile(id).await {
37        Ok(r) => Redirect::to(&format!("/@{}", r.username)).into_response(),
38        Err(_) => (
39            axum::http::StatusCode::NOT_FOUND,
40            [(axum::http::header::CONTENT_TYPE, "text/html")],
41            DatabaseError::NotFound.to_html(database),
42        )
43            .into_response(),
44    }
45}
46
47/// Redirect an IP to a full username
48pub async fn expand_ip_request(
49    jar: CookieJar,
50    Path(ip): Path<String>,
51    State(database): State<Database>,
52) -> impl IntoResponse {
53    // get user from token
54    match jar.get("__Secure-Token") {
55        Some(c) => {
56            if let Err(_) = database
57                .auth
58                .get_profile_by_unhashed(c.value_trimmed())
59                .await
60            {
61                return Redirect::to("/");
62            }
63        }
64        None => {
65            return Redirect::to("/");
66        }
67    };
68
69    // return
70    match database.auth.get_profile_by_ip(&ip).await {
71        Ok(r) => Redirect::to(&format!("/@{}", r.username)),
72        Err(_) => Redirect::to("/"),
73    }
74}
75
76/// Report a user profile
77pub async fn report_request(
78    headers: HeaderMap,
79    Path(input): Path<String>,
80    State(database): State<Database>,
81    Json(req): Json<super::CreateReport>,
82) -> impl IntoResponse {
83    // check hcaptcha
84    if let Err(e) = req
85        .valid_response(&database.config.captcha.secret, None)
86        .await
87    {
88        return Json(DefaultReturn {
89            success: false,
90            message: e.to_string(),
91            payload: (),
92        });
93    }
94
95    // get user
96    let profile = match database.get_profile(input.clone()).await {
97        Ok(p) => p,
98        Err(e) => return Json(e.to_json()),
99    };
100
101    // get real ip
102    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
103        headers
104            .get(real_ip_header.to_owned())
105            .unwrap_or(&HeaderValue::from_static(""))
106            .to_str()
107            .unwrap_or("")
108            .to_string()
109    } else {
110        String::new()
111    };
112
113    // check ip
114    if database.auth.get_ipban_by_ip(&real_ip).await.is_ok() {
115        return Json(DefaultReturn {
116            success: false,
117            message: DatabaseError::Banned.to_string(),
118            payload: (),
119        });
120    }
121
122    // report
123    match database
124        .auth
125        .create_notification(
126            NotificationCreate {
127                title: format!("**PROFILE REPORT**: [/@{input}](/+u/{})", profile.id),
128                content: format!("{}\n\n***\n\n[{real_ip}](/+i/{real_ip})", req.content),
129                address: format!("/@{input}"),
130                recipient: "*".to_string(), // all staff
131            },
132            None,
133        )
134        .await
135    {
136        Ok(_) => {
137            return Json(DefaultReturn {
138                success: true,
139                message: "Profile reported!".to_string(),
140                payload: (),
141            })
142        }
143        Err(_) => Json(DefaultReturn {
144            success: false,
145            message: DatabaseError::NotFound.to_string(),
146            payload: (),
147        }),
148    }
149}
150
151/// Create a data export of the given user
152pub async fn export_request(
153    jar: CookieJar,
154    Path(username): Path<String>,
155    State(database): State<Database>,
156    Query(props): Query<DataExportOptions>,
157) -> impl IntoResponse {
158    // get user from token
159    let auth_user = match jar.get("__Secure-Token") {
160        Some(c) => match database
161            .auth
162            .get_profile_by_unhashed(c.value_trimmed())
163            .await
164        {
165            Ok(ua) => ua,
166            Err(e) => return Json(e.to_json()),
167        },
168        None => return Json(DatabaseError::NotAllowed.to_json()),
169    };
170
171    let group = match database.auth.get_group_by_id(auth_user.group).await {
172        Ok(g) => g,
173        Err(_) => {
174            return Json(DefaultReturn {
175                success: false,
176                message: DatabaseError::Other.to_string(),
177                payload: None,
178            })
179        }
180    };
181
182    if !group.permissions.check(FinePermission::EXPORT_DATA) {
183        return Json(DefaultReturn {
184            success: false,
185            message: DatabaseError::NotAllowed.to_string(),
186            payload: None,
187        });
188    }
189
190    // ...
191    let other_user = match database.auth.get_profile_by_username(&username).await {
192        Ok(ua) => ua,
193        Err(_) => {
194            return Json(DefaultReturn {
195                success: false,
196                message: DatabaseError::NotFound.to_string(),
197                payload: None,
198            })
199        }
200    };
201
202    // return
203    match database.create_data_export(other_user.id, props).await {
204        Ok(export) => {
205            return Json(DefaultReturn {
206                success: true,
207                message: "Acceptable".to_string(),
208                payload: Some(export),
209            })
210        }
211        Err(e) => return Json(e.to_json()),
212    }
213}
214
215/// IP block a profile
216pub async fn ipblock_request(
217    jar: CookieJar,
218    Path(id): Path<String>,
219    State(database): State<Database>,
220) -> impl IntoResponse {
221    // get user from token
222    let auth_user = match jar.get("__Secure-Token") {
223        Some(c) => match database
224            .auth
225            .get_profile_by_unhashed(c.value_trimmed())
226            .await
227        {
228            Ok(ua) => ua,
229            Err(_) => {
230                return Json(DatabaseError::NotAllowed.into());
231            }
232        },
233        None => {
234            return Json(DatabaseError::NotAllowed.into());
235        }
236    };
237
238    // get profile
239    let profile = match database.auth.get_profile(&id).await {
240        Ok(p) => p,
241        Err(e) => return Json(e.to_json()),
242    };
243
244    // block
245    for ip in profile.ips {
246        if let Err(_) = database
247            .auth
248            .create_ipblock(
249                IpBlockCreate {
250                    ip,
251                    context: profile.username.clone(),
252                },
253                auth_user.clone(),
254            )
255            .await
256        {
257            continue;
258        }
259    }
260
261    return Json(DefaultReturn {
262        success: true,
263        message: "IPs blocked!".to_string(),
264        payload: (),
265    });
266}