authbeam/api/
me.rs

1use crate::database::Database;
2use crate::model::{DatabaseError, TokenContext, TokenPermission};
3use serde::{Deserialize, Serialize};
4use databeam::prelude::DefaultReturn;
5
6use axum::http::{HeaderMap, HeaderValue};
7use axum::response::{IntoResponse, Redirect};
8use axum::{extract::State, Json};
9use axum_extra::extract::cookie::CookieJar;
10use pathbufd::pathd;
11
12use crate::avif::{save_avif_buffer, Image};
13
14/// Returns the current user's username
15pub async fn get_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
16    // get user from token
17    let auth_user = match jar.get("__Secure-Token") {
18        Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
19            Ok(ua) => ua,
20            Err(e) => return Json(e.to_json()),
21        },
22        None => return Json(DatabaseError::NotAllowed.to_json()),
23    };
24
25    // return
26    Json(DefaultReturn {
27        success: true,
28        message: auth_user.id.to_string(),
29        payload: Some(auth_user),
30    })
31}
32
33#[derive(Serialize, Deserialize)]
34pub struct DeleteProfile {
35    password: String,
36    #[serde(default)]
37    totp: String,
38}
39
40/// Delete the current user's profile
41pub async fn delete_request(
42    jar: CookieJar,
43    State(database): State<Database>,
44    Json(props): Json<DeleteProfile>,
45) -> impl IntoResponse {
46    // get user from token
47    let auth_user = match jar.get("__Secure-Token") {
48        Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
49            Ok(ua) => ua,
50            Err(e) => return Json(e.to_json()),
51        },
52        None => return Json(DatabaseError::NotAllowed.to_json()),
53    };
54
55    // get profile
56    let hashed = rainbeam_shared::hash::hash_salted(props.password, auth_user.salt.clone());
57
58    if hashed != auth_user.password {
59        return Json(DefaultReturn {
60            success: false,
61            message: DatabaseError::NotAllowed.to_string(),
62            payload: (),
63        });
64    }
65
66    // check totp
67    if !database.check_totp(&auth_user, &props.totp) {
68        return Json(DatabaseError::NotAllowed.to_json());
69    }
70
71    // return
72    if let Err(e) = database.delete_profile_by_id(&auth_user.id).await {
73        return Json(e.to_json());
74    }
75
76    Json(DefaultReturn {
77        success: true,
78        message: "Profile deleted, goodbye!".to_string(),
79        payload: (),
80    })
81}
82
83/// Generate a new token and session (like logging in while already logged in)
84pub async fn generate_token_request(
85    jar: CookieJar,
86    headers: HeaderMap,
87    State(database): State<Database>,
88    Json(props): Json<TokenContext>,
89) -> impl IntoResponse {
90    // get user from token
91    let mut existing_permissions: Option<Vec<TokenPermission>> = None;
92    let mut auth_user = match jar.get("__Secure-Token") {
93        Some(c) => {
94            let token = c.value_trimmed();
95
96            match database.get_profile_by_unhashed(token).await {
97                Ok(ua) => {
98                    // check token permission
99                    let token = ua.token_context_from_token(&token);
100
101                    if let Some(ref permissions) = token.permissions {
102                        existing_permissions = Some(permissions.to_owned())
103                    }
104
105                    if !token.can_do(TokenPermission::GenerateTokens) {
106                        return Json(DatabaseError::NotAllowed.to_json());
107                    }
108
109                    // return
110                    ua
111                }
112                Err(e) => return Json(e.to_json()),
113            }
114        }
115        None => return Json(DatabaseError::NotAllowed.to_json()),
116    };
117
118    // for every token that doesn't have a context, insert the default context
119    for (i, _) in auth_user.tokens.clone().iter().enumerate() {
120        if let None = auth_user.token_context.get(i) {
121            auth_user.token_context.insert(i, TokenContext::default())
122        }
123    }
124
125    // get real ip
126    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
127        headers
128            .get(real_ip_header.to_owned())
129            .unwrap_or(&HeaderValue::from_static(""))
130            .to_str()
131            .unwrap_or("")
132            .to_string()
133    } else {
134        String::new()
135    };
136
137    // check ip
138    if database.get_ipban_by_ip(&real_ip).await.is_ok() {
139        return Json(DefaultReturn {
140            success: false,
141            message: DatabaseError::NotAllowed.to_string(),
142            payload: None,
143        });
144    }
145
146    // check given context
147    if let Some(ref permissions) = props.permissions {
148        // make sure we don't want anything we don't have
149        // if our permissions are "None", allow any permission to be granted
150        for permission in permissions {
151            if let Some(ref existing) = existing_permissions {
152                if !existing.contains(permission) {
153                    return Json(DefaultReturn {
154                        success: false,
155                        message: DatabaseError::OutOfScope.to_string(),
156                        payload: None,
157                    });
158                }
159            } else {
160                break;
161            }
162        }
163    }
164
165    // ...
166    let token = databeam::utility::uuid();
167    let token_hashed = databeam::utility::hash(token.clone());
168
169    auth_user.tokens.push(token_hashed);
170    auth_user.ips.push(String::new()); // don't actually store ip, this endpoint is used by external apps
171    auth_user.token_context.push(props);
172
173    database
174        .update_profile_tokens(
175            &auth_user.id,
176            auth_user.tokens,
177            auth_user.ips,
178            auth_user.token_context,
179        )
180        .await
181        .unwrap();
182
183    // return
184    return Json(DefaultReturn {
185        success: true,
186        message: "Generated token!".to_string(),
187        payload: Some(token),
188    });
189}
190
191#[derive(Serialize, Deserialize)]
192pub struct UpdateTokens {
193    pub tokens: Vec<String>,
194}
195
196/// Update the current user's session tokens
197pub async fn update_tokens_request(
198    jar: CookieJar,
199    State(database): State<Database>,
200    Json(req): Json<UpdateTokens>,
201) -> impl IntoResponse {
202    // get user from token
203    let mut auth_user = match jar.get("__Secure-Token") {
204        Some(c) => {
205            let token = c.value_trimmed();
206
207            match database.get_profile_by_unhashed(token).await {
208                Ok(ua) => {
209                    // check token permission
210                    if !ua
211                        .token_context_from_token(&token)
212                        .can_do(TokenPermission::ManageAccount)
213                    {
214                        return Json(DatabaseError::NotAllowed.to_json());
215                    }
216
217                    // return
218                    ua
219                }
220                Err(e) => return Json(e.to_json()),
221            }
222        }
223        None => return Json(DatabaseError::NotAllowed.to_json()),
224    };
225
226    // for every token that doesn't have a context, insert the default context
227    for (i, _) in auth_user.tokens.clone().iter().enumerate() {
228        if let None = auth_user.token_context.get(i) {
229            auth_user.token_context.insert(i, TokenContext::default())
230        }
231    }
232
233    // get diff
234    let mut removed_indexes = Vec::new();
235
236    for (i, token) in auth_user.tokens.iter().enumerate() {
237        if !req.tokens.contains(token) {
238            removed_indexes.push(i);
239        }
240    }
241
242    // edit dependent vecs
243    for i in removed_indexes.clone() {
244        if (auth_user.ips.len() < i) | (auth_user.ips.len() == 0) {
245            break;
246        }
247
248        auth_user.ips.remove(i);
249    }
250
251    for i in removed_indexes.clone() {
252        if (auth_user.token_context.len() < i) | (auth_user.token_context.len() == 0) {
253            break;
254        }
255
256        auth_user.token_context.remove(i);
257    }
258
259    // return
260    if let Err(e) = database
261        .update_profile_tokens(
262            &auth_user.id,
263            req.tokens,
264            auth_user.ips,
265            auth_user.token_context,
266        )
267        .await
268    {
269        return Json(e.to_json());
270    }
271
272    Json(DefaultReturn {
273        success: true,
274        message: "Tokens updated!".to_string(),
275        payload: (),
276    })
277}
278
279static MAXIUMUM_FILE_SIZE: usize = 8388608;
280
281/// Upload avatar
282pub async fn upload_avatar_request(
283    jar: CookieJar,
284    State(database): State<Database>,
285    img: Image,
286) -> impl IntoResponse {
287    // get user from token
288    let mut auth_user = match jar.get("__Secure-Token") {
289        Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
290            Ok(ua) => ua,
291            Err(e) => {
292                return Redirect::to(&format!(
293                    "/settings/profile?ANNC={}&ANNC_TYPE=caution",
294                    e.to_string()
295                ));
296            }
297        },
298        None => {
299            return Redirect::to(&format!(
300                "/settings/profile?ANNC={}&ANNC_TYPE=caution",
301                DatabaseError::NotFound.to_string()
302            ));
303        }
304    };
305
306    let path = pathd!(
307        "{}/avatars/{}.avif",
308        database.config.media_dir,
309        &auth_user.id
310    );
311
312    // check file size
313    if img.0.len() > MAXIUMUM_FILE_SIZE {
314        return Redirect::to(&format!(
315            "/settings/profile?ANNC={}&ANNC_TYPE=caution",
316            DatabaseError::TooLong.to_string()
317        ));
318    }
319
320    // upload image
321    let mut bytes = Vec::new();
322
323    for byte in img.0 {
324        bytes.push(byte);
325    }
326
327    match save_avif_buffer(&path, bytes) {
328        Ok(_) => {
329            // update profile config
330            auth_user
331                .metadata
332                .kv
333                .insert("sparkler:avatar_url".to_string(), "rb://".to_string());
334
335            match database
336                .update_profile_metadata(&auth_user.id, auth_user.metadata)
337                .await
338            {
339                Ok(_) => Redirect::to(&format!(
340                    "/settings/profile?ANNC={}&ANNC_TYPE=tip",
341                    "File uploaded"
342                )),
343                Err(e) => Redirect::to(&format!(
344                    "/settings/profile?ANNC={}&ANNC_TYPE=caution",
345                    e.to_string()
346                )),
347            }
348        }
349        Err(e) => Redirect::to(&format!(
350            "/settings/profile?ANNC={}&ANNC_TYPE=caution",
351            e.to_string()
352        )),
353    }
354}
355
356/// Upload banner
357pub async fn upload_banner_request(
358    jar: CookieJar,
359    State(database): State<Database>,
360    img: Image,
361) -> impl IntoResponse {
362    // get user from token
363    let mut auth_user = match jar.get("__Secure-Token") {
364        Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
365            Ok(ua) => ua,
366            Err(e) => {
367                return Redirect::to(&format!(
368                    "/settings/profile?ANNC={}&ANNC_TYPE=caution",
369                    e.to_string()
370                ));
371            }
372        },
373        None => {
374            return Redirect::to(&format!(
375                "/settings/profile?ANNC={}&ANNC_TYPE=caution",
376                DatabaseError::NotFound.to_string()
377            ));
378        }
379    };
380
381    let path = pathd!(
382        "{}/banners/{}.avif",
383        database.config.media_dir,
384        &auth_user.id
385    );
386
387    // check file size
388    if img.0.len() > MAXIUMUM_FILE_SIZE {
389        return Redirect::to(&format!(
390            "/settings/profile?ANNC={}&ANNC_TYPE=caution",
391            DatabaseError::TooLong.to_string()
392        ));
393    }
394
395    // upload image
396    let mut bytes = Vec::new();
397
398    for byte in img.0 {
399        bytes.push(byte);
400    }
401
402    match save_avif_buffer(&path, bytes) {
403        Ok(_) => {
404            // update profile config
405            auth_user
406                .metadata
407                .kv
408                .insert("sparkler:banner_url".to_string(), "rb://".to_string());
409
410            match database
411                .update_profile_metadata(&auth_user.id, auth_user.metadata)
412                .await
413            {
414                Ok(_) => Redirect::to(&format!(
415                    "/settings/profile?ANNC={}&ANNC_TYPE=tip",
416                    "File uploaded"
417                )),
418                Err(e) => Redirect::to(&format!(
419                    "/settings/profile?ANNC={}&ANNC_TYPE=caution",
420                    e.to_string()
421                )),
422            }
423        }
424        Err(e) => Redirect::to(&format!(
425            "/settings/profile?ANNC={}&ANNC_TYPE=caution",
426            e.to_string()
427        )),
428    }
429}