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
14pub async fn get_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
16 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 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
40pub async fn delete_request(
42 jar: CookieJar,
43 State(database): State<Database>,
44 Json(props): Json<DeleteProfile>,
45) -> impl IntoResponse {
46 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 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 if !database.check_totp(&auth_user, &props.totp) {
68 return Json(DatabaseError::NotAllowed.to_json());
69 }
70
71 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
83pub 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 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 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 ua
111 }
112 Err(e) => return Json(e.to_json()),
113 }
114 }
115 None => return Json(DatabaseError::NotAllowed.to_json()),
116 };
117
118 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 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 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 if let Some(ref permissions) = props.permissions {
148 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 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()); 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 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
196pub async fn update_tokens_request(
198 jar: CookieJar,
199 State(database): State<Database>,
200 Json(req): Json<UpdateTokens>,
201) -> impl IntoResponse {
202 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 if !ua
211 .token_context_from_token(&token)
212 .can_do(TokenPermission::ManageAccount)
213 {
214 return Json(DatabaseError::NotAllowed.to_json());
215 }
216
217 ua
219 }
220 Err(e) => return Json(e.to_json()),
221 }
222 }
223 None => return Json(DatabaseError::NotAllowed.to_json()),
224 };
225
226 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 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 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 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
281pub async fn upload_avatar_request(
283 jar: CookieJar,
284 State(database): State<Database>,
285 img: Image,
286) -> impl IntoResponse {
287 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 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 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 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
356pub async fn upload_banner_request(
358 jar: CookieJar,
359 State(database): State<Database>,
360 img: Image,
361) -> impl IntoResponse {
362 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 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 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 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}