1use crate::database::Database;
2use crate::model::{
3 DatabaseError, FinePermission, NotificationCreate, RenderLayout, SetProfileBadges,
4 SetProfileCoins, SetProfileGroup, SetProfileLabels, SetProfileLayout, SetProfileLinks,
5 SetProfileMetadata, SetProfilePassword, SetProfileTier, SetProfileUsername, TOTPDisable,
6 TokenContext, TokenPermission,
7};
8use crate::simplify;
9use databeam::prelude::DefaultReturn;
10use pathbufd::pathd;
11
12use axum::body::Body;
13use axum::http::{HeaderMap, HeaderValue};
14use axum::response::IntoResponse;
15use axum::{
16 extract::{Path, State},
17 Json,
18};
19use axum_extra::extract::cookie::CookieJar;
20use serde::Serialize;
21
22use std::{fs::File, io::Read};
23
24pub fn read_image(static_dir: String, image: String) -> Vec<u8> {
25 let mut bytes = Vec::new();
26
27 for byte in match File::open(format!("{static_dir}/{image}")) {
28 Ok(f) => f,
29 Err(_) => return bytes,
30 }
31 .bytes()
32 {
33 bytes.push(byte.unwrap())
34 }
35
36 bytes
37}
38
39pub async fn avatar_request(
41 Path(id): Path<String>,
42 State(database): State<Database>,
43) -> impl IntoResponse {
44 let auth_user = match database.get_profile(&id).await {
46 Ok(ua) => ua,
47 Err(_) => {
48 return (
49 [("Content-Type", "image/svg+xml")],
50 Body::from(read_image(
51 pathd!("{}/images", database.config.static_dir),
52 "default-avatar.svg".to_string(),
53 )),
54 );
55 }
56 };
57
58 let avatar_url = match auth_user.metadata.kv.get("sparkler:avatar_url") {
60 Some(r) => r,
61 None => "",
62 };
63
64 if (avatar_url == "rb://") && !database.config.media_dir.to_string().is_empty() {
65 return (
66 [("Content-Type", "image/avif")],
67 Body::from(read_image(
68 pathd!("{}/avatars", database.config.media_dir),
69 format!("{}.avif", &auth_user.id),
70 )),
71 );
72 }
73
74 if avatar_url.starts_with(&database.config.host) {
75 return (
76 [("Content-Type", "image/svg+xml")],
77 Body::from(read_image(
78 pathd!("{}/images", database.config.static_dir),
79 "default-avatar.svg".to_string(),
80 )),
81 );
82 }
83
84 for host in database.config.blocked_hosts {
85 if avatar_url.starts_with(&host) {
86 return (
87 [("Content-Type", "image/svg+xml")],
88 Body::from(read_image(
89 pathd!("{}/images", database.config.static_dir),
90 "default-avatar.svg".to_string(),
91 )),
92 );
93 }
94 }
95
96 if avatar_url.is_empty() {
98 return (
99 [("Content-Type", "image/svg+xml")],
100 Body::from(read_image(
101 pathd!("{}/images", database.config.static_dir),
102 "default-avatar.svg".to_string(),
103 )),
104 );
105 }
106
107 let guessed_mime = mime_guess::from_path(avatar_url)
108 .first_raw()
109 .unwrap_or("application/octet-stream");
110
111 match database.http.get(avatar_url).send().await {
112 Ok(stream) => {
113 let size = stream.content_length();
114 if size.unwrap_or_default() > 10485760 {
115 return (
117 [("Content-Type", "image/svg+xml")],
118 Body::from(read_image(
119 pathd!("{}/images", database.config.static_dir),
120 "default-banner.svg".to_string(),
121 )),
122 );
123 }
124
125 if let Some(ct) = stream.headers().get("Content-Type") {
126 let ct = ct.to_str().unwrap();
127 let bad_ct = vec!["text/html", "text/plain"];
128 if (!ct.starts_with("image/") && !ct.starts_with("font/")) | bad_ct.contains(&ct) {
129 return (
131 [("Content-Type", "image/svg+xml")],
132 Body::from(read_image(
133 pathd!("{}/images", database.config.static_dir),
134 "default-banner.svg".to_string(),
135 )),
136 );
137 }
138 }
139
140 (
141 [(
142 "Content-Type",
143 if guessed_mime == "text/html" {
144 "text/plain"
145 } else {
146 guessed_mime
147 },
148 )],
149 Body::from_stream(stream.bytes_stream()),
150 )
151 }
152 Err(_) => (
153 [("Content-Type", "image/svg+xml")],
154 Body::from(read_image(
155 pathd!("{}/images", database.config.static_dir),
156 "default-avatar.svg".to_string(),
157 )),
158 ),
159 }
160}
161
162pub async fn banner_request(
164 Path(id): Path<String>,
165 State(database): State<Database>,
166) -> impl IntoResponse {
167 let auth_user = match database.get_profile(&id).await {
169 Ok(ua) => ua,
170 Err(_) => {
171 return (
172 [("Content-Type", "image/svg+xml")],
173 Body::from(read_image(
174 pathd!("{}/images", database.config.static_dir),
175 "default-banner.svg".to_string(),
176 )),
177 );
178 }
179 };
180
181 let banner_url = match auth_user.metadata.kv.get("sparkler:banner_url") {
183 Some(r) => r,
184 None => "",
185 };
186
187 if (banner_url == "rb://") && !database.config.media_dir.to_string().is_empty() {
188 return (
189 [("Content-Type", "image/avif")],
190 Body::from(read_image(
191 pathd!("{}/banners", database.config.media_dir),
192 format!("{}.avif", &auth_user.id),
193 )),
194 );
195 }
196
197 if banner_url.starts_with(&database.config.host) {
198 return (
199 [("Content-Type", "image/svg+xml")],
200 Body::from(read_image(
201 pathd!("{}/images", database.config.static_dir),
202 "default-banner.svg".to_string(),
203 )),
204 );
205 }
206
207 for host in database.config.blocked_hosts {
208 if banner_url.starts_with(&host) {
209 return (
210 [("Content-Type", "image/svg+xml")],
211 Body::from(read_image(
212 pathd!("{}/images", database.config.static_dir),
213 "default-banner.svg".to_string(),
214 )),
215 );
216 }
217 }
218
219 if banner_url.is_empty() {
221 return (
222 [("Content-Type", "image/svg+xml")],
223 Body::from(read_image(
224 pathd!("{}/images", database.config.static_dir),
225 "default-banner.svg".to_string(),
226 )),
227 );
228 }
229
230 let guessed_mime = mime_guess::from_path(banner_url)
231 .first_raw()
232 .unwrap_or("application/octet-stream");
233
234 match database.http.get(banner_url).send().await {
235 Ok(stream) => {
236 let size = stream.content_length();
237 if size.unwrap_or_default() > 10485760 {
238 return (
240 [("Content-Type", "image/svg+xml")],
241 Body::from(read_image(
242 pathd!("{}/images", database.config.static_dir),
243 "default-banner.svg".to_string(),
244 )),
245 );
246 }
247
248 if let Some(ct) = stream.headers().get("Content-Type") {
249 let ct = ct.to_str().unwrap();
250 let bad_ct = vec!["text/html", "text/plain"];
251 if (!ct.starts_with("image/") && !ct.starts_with("font/")) | bad_ct.contains(&ct) {
252 return (
254 [("Content-Type", "image/svg+xml")],
255 Body::from(read_image(
256 pathd!("{}/images", database.config.static_dir),
257 "default-banner.svg".to_string(),
258 )),
259 );
260 }
261 }
262
263 (
264 [(
265 "Content-Type",
266 if guessed_mime == "text/html" {
267 "text/plain"
268 } else {
269 guessed_mime
270 },
271 )],
272 Body::from_stream(stream.bytes_stream()),
273 )
274 }
275 Err(_) => (
276 [("Content-Type", "image/svg+xml")],
277 Body::from(read_image(
278 pathd!("{}/images", database.config.static_dir),
279 "default-banner.svg".to_string(),
280 )),
281 ),
282 }
283}
284
285pub async fn get_request(
287 Path(id): Path<String>,
288 State(database): State<Database>,
289) -> impl IntoResponse {
290 let mut auth_user = match database.get_profile(&id).await {
292 Ok(ua) => ua,
293 Err(e) => return Json(e.to_json()),
294 };
295
296 auth_user.clean();
298
299 Json(DefaultReturn {
301 success: true,
302 message: auth_user.id.to_string(),
303 payload: Some(auth_user),
304 })
305}
306
307pub async fn get_from_token_request(
309 Path(token): Path<String>,
310 State(database): State<Database>,
311) -> impl IntoResponse {
312 let auth_user = match database.get_profile_by_unhashed(&token).await {
314 Ok(ua) => ua,
315 Err(e) => return Json(e.to_json()),
316 };
317
318 Json(DefaultReturn {
320 success: true,
321 message: auth_user.username.to_string(),
322 payload: Some(auth_user),
323 })
324}
325
326pub async fn update_tier_request(
328 jar: CookieJar,
329 Path(id): Path<String>,
330 State(database): State<Database>,
331 Json(props): Json<SetProfileTier>,
332) -> impl IntoResponse {
333 let auth_user = match jar.get("__Secure-Token") {
335 Some(c) => {
336 let token = c.value_trimmed();
337
338 match database.get_profile_by_unhashed(token).await {
339 Ok(ua) => {
340 if !ua
342 .token_context_from_token(&token)
343 .can_do(TokenPermission::Moderator)
344 {
345 return Json(DatabaseError::NotAllowed.to_json());
346 }
347
348 ua
350 }
351 Err(e) => return Json(e.to_json()),
352 }
353 }
354 None => return Json(DatabaseError::NotAllowed.to_json()),
355 };
356
357 let group = match database.get_group_by_id(auth_user.group).await {
359 Ok(g) => g,
360 Err(e) => return Json(e.to_json()),
361 };
362
363 if !group.permissions.check(FinePermission::MANAGE_PROFILE_TIER) {
364 return Json(DefaultReturn {
366 success: false,
367 message: DatabaseError::NotAllowed.to_string(),
368 payload: None,
369 });
370 }
371
372 let other_user = match database.get_profile(&id).await {
374 Ok(ua) => ua,
375 Err(e) => return Json(e.to_json()),
376 };
377
378 let group = match database.get_group_by_id(other_user.group).await {
380 Ok(g) => g,
381 Err(e) => return Json(e.to_json()),
382 };
383
384 if group.permissions.check(FinePermission::MANAGE_PROFILE_TIER) {
385 return Json(DefaultReturn {
386 success: false,
387 message: DatabaseError::NotAllowed.to_string(),
388 payload: None,
389 });
390 }
391
392 if let Err(e) = database.update_profile_tier(&id, props.tier).await {
395 return Json(e.to_json());
396 }
397
398 Json(DefaultReturn {
400 success: true,
401 message: "Acceptable".to_string(),
402 payload: Some(props.tier),
403 })
404}
405
406pub async fn update_group_request(
408 jar: CookieJar,
409 Path(id): Path<String>,
410 State(database): State<Database>,
411 Json(props): Json<SetProfileGroup>,
412) -> impl IntoResponse {
413 let auth_user = match jar.get("__Secure-Token") {
415 Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
416 Ok(ua) => ua,
417 Err(e) => return Json(e.to_json()),
418 },
419 None => return Json(DatabaseError::NotAllowed.to_json()),
420 };
421
422 let our_group = match database.get_group_by_id(auth_user.group).await {
424 Ok(g) => g,
425 Err(e) => return Json(e.to_json()),
426 };
427
428 if !our_group
429 .permissions
430 .check(FinePermission::MANAGE_PROFILE_GROUP)
431 {
432 return Json(DatabaseError::NotAllowed.to_json());
434 }
435
436 let other_user = match database.get_profile(&id).await {
438 Ok(ua) => ua,
439 Err(e) => return Json(e.to_json()),
440 };
441
442 let other_group = match database.get_group_by_id(other_user.group).await {
444 Ok(g) => g,
445 Err(e) => return Json(e.to_json()),
446 };
447
448 if other_group
449 .permissions
450 .check(FinePermission::MANAGE_PROFILE_GROUP)
451 {
452 return Json(DatabaseError::NotAllowed.to_json());
453 }
454
455 if props.group != -1 {
457 if let Err(e) = database.get_group_by_id(props.group).await {
458 return Json(e.to_json());
459 }
460
461 if !our_group.permissions.check(FinePermission::PROMOTE_USERS) {
462 return Json(DatabaseError::NotAllowed.to_json());
464 }
465 }
466
467 if let Err(e) = database
470 .update_profile_group(&other_user.id, props.group)
471 .await
472 {
473 return Json(e.to_json());
474 }
475
476 if let Err(e) = database
478 .audit(
479 &auth_user.id,
480 &format!(
481 "Changed user group: [{}](/+u/{})",
482 other_user.id, other_user.id
483 ),
484 )
485 .await
486 {
487 return Json(e.to_json());
488 };
489
490 Json(DefaultReturn {
491 success: true,
492 message: "Acceptable".to_string(),
493 payload: Some(props.group),
494 })
495}
496
497pub async fn update_coins_request(
499 jar: CookieJar,
500 Path(id): Path<String>,
501 State(database): State<Database>,
502 Json(props): Json<SetProfileCoins>,
503) -> impl IntoResponse {
504 let auth_user = match jar.get("__Secure-Token") {
506 Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
507 Ok(ua) => ua,
508 Err(e) => return Json(e.to_json()),
509 },
510 None => return Json(DatabaseError::NotAllowed.to_json()),
511 };
512
513 let group = match database.get_group_by_id(auth_user.group).await {
515 Ok(g) => g,
516 Err(e) => return Json(e.to_json()),
517 };
518
519 if !group.permissions.check(FinePermission::ECON_MASTER) {
520 return Json(DefaultReturn {
522 success: false,
523 message: DatabaseError::NotAllowed.to_string(),
524 payload: None,
525 });
526 }
527
528 let other_user = match database.get_profile(&id).await {
530 Ok(ua) => ua,
531 Err(e) => return Json(e.to_json()),
532 };
533
534 if let Err(e) = database
537 .update_profile_coins(&other_user.id, props.coins)
538 .await
539 {
540 return Json(e.to_json());
541 }
542
543 if let Err(e) = database
545 .audit(
546 &auth_user.id,
547 &format!(
548 "Updated user coin balance: [{}](/+u/{})",
549 other_user.id, other_user.id
550 ),
551 )
552 .await
553 {
554 return Json(e.to_json());
555 };
556
557 Json(DefaultReturn {
558 success: true,
559 message: "Acceptable".to_string(),
560 payload: Some(props.coins),
561 })
562}
563
564pub async fn update_tokens_request(
566 jar: CookieJar,
567 Path(id): Path<String>,
568 State(database): State<Database>,
569 Json(req): Json<super::me::UpdateTokens>,
570) -> impl IntoResponse {
571 let auth_user = match jar.get("__Secure-Token") {
573 Some(c) => {
574 let token = c.value_trimmed();
575
576 match database.get_profile_by_unhashed(token).await {
577 Ok(ua) => {
578 if !ua
580 .token_context_from_token(&token)
581 .can_do(TokenPermission::Moderator)
582 {
583 return Json(DatabaseError::NotAllowed.to_json());
584 }
585
586 ua
588 }
589 Err(e) => return Json(e.to_json()),
590 }
591 }
592 None => return Json(DatabaseError::NotAllowed.to_json()),
593 };
594
595 let mut other = match database.get_profile(&id).await {
596 Ok(o) => o,
597 Err(e) => return Json(e.to_json()),
598 };
599
600 if auth_user.id == other.id {
601 return Json(DefaultReturn {
602 success: false,
603 message: DatabaseError::NotAllowed.to_string(),
604 payload: (),
605 });
606 }
607
608 let group = match database.get_group_by_id(auth_user.group).await {
609 Ok(g) => g,
610 Err(e) => return Json(e.to_json()),
611 };
612
613 if !group.permissions.check(FinePermission::EDIT_USER) {
614 return Json(DefaultReturn {
616 success: false,
617 message: DatabaseError::NotAllowed.to_string(),
618 payload: (),
619 });
620 }
621
622 let group = match database.get_group_by_id(other.group).await {
624 Ok(g) => g,
625 Err(e) => return Json(e.to_json()),
626 };
627
628 if group.permissions.check(FinePermission::ADMINISTRATOR) {
629 return Json(DefaultReturn {
631 success: false,
632 message: DatabaseError::NotAllowed.to_string(),
633 payload: (),
634 });
635 }
636
637 for (i, _) in other.tokens.clone().iter().enumerate() {
639 if let None = other.token_context.get(i) {
640 other.token_context.insert(i, TokenContext::default())
641 }
642 }
643
644 let mut removed_indexes = Vec::new();
646
647 for (i, token) in other.tokens.iter().enumerate() {
648 if !req.tokens.contains(token) {
649 removed_indexes.push(i);
650 }
651 }
652
653 for i in removed_indexes.clone() {
655 if (other.ips.len() < i) | (other.ips.len() == 0) {
656 break;
657 }
658
659 other.ips.remove(i);
660 }
661
662 for i in removed_indexes.clone() {
663 if (other.token_context.len() < i) | (other.token_context.len() == 0) {
664 break;
665 }
666
667 other.token_context.remove(i);
668 }
669
670 if let Err(e) = database
672 .update_profile_tokens(&other.id, req.tokens, other.ips, other.token_context)
673 .await
674 {
675 return Json(e.to_json());
676 }
677
678 Json(DefaultReturn {
679 success: true,
680 message: "Tokens updated!".to_string(),
681 payload: (),
682 })
683}
684
685pub async fn generate_token_request(
687 jar: CookieJar,
688 headers: HeaderMap,
689 Path(id): Path<String>,
690 State(database): State<Database>,
691 Json(props): Json<TokenContext>,
692) -> impl IntoResponse {
693 let auth_user = match jar.get("__Secure-Token") {
695 Some(c) => {
696 let token = c.value_trimmed();
697
698 match database.get_profile_by_unhashed(token).await {
699 Ok(ua) => {
700 if !ua
702 .token_context_from_token(&token)
703 .can_do(TokenPermission::Moderator)
704 {
705 return Json(DatabaseError::NotAllowed.to_json());
706 }
707
708 ua
710 }
711 Err(e) => return Json(e.to_json()),
712 }
713 }
714 None => return Json(DatabaseError::NotAllowed.to_json()),
715 };
716
717 let mut other = match database.get_profile(&id).await {
718 Ok(o) => o,
719 Err(e) => return Json(e.to_json()),
720 };
721
722 if auth_user.id == other.id {
723 return Json(DefaultReturn {
724 success: false,
725 message: DatabaseError::NotAllowed.to_string(),
726 payload: None,
727 });
728 }
729
730 let group = match database.get_group_by_id(auth_user.group).await {
731 Ok(g) => g,
732 Err(e) => return Json(e.to_json()),
733 };
734
735 if !group.permissions.check(FinePermission::EDIT_USER) {
736 return Json(DefaultReturn {
738 success: false,
739 message: DatabaseError::NotAllowed.to_string(),
740 payload: None,
741 });
742 }
743
744 let group = match database.get_group_by_id(other.group).await {
746 Ok(g) => g,
747 Err(e) => return Json(e.to_json()),
748 };
749
750 if group.permissions.check(FinePermission::ADMINISTRATOR) {
751 return Json(DefaultReturn {
753 success: false,
754 message: DatabaseError::NotAllowed.to_string(),
755 payload: None,
756 });
757 }
758
759 for (i, _) in other.tokens.clone().iter().enumerate() {
761 if let None = other.token_context.get(i) {
762 other.token_context.insert(i, TokenContext::default())
763 }
764 }
765
766 let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
768 headers
769 .get(real_ip_header.to_owned())
770 .unwrap_or(&HeaderValue::from_static(""))
771 .to_str()
772 .unwrap_or("")
773 .to_string()
774 } else {
775 String::new()
776 };
777
778 if database.get_ipban_by_ip(&real_ip).await.is_ok() {
780 return Json(DefaultReturn {
781 success: false,
782 message: DatabaseError::NotAllowed.to_string(),
783 payload: None,
784 });
785 }
786
787 let token = databeam::utility::uuid();
789 let token_hashed = databeam::utility::hash(token.clone());
790
791 other.tokens.push(token_hashed);
792 other.ips.push(String::new()); other.token_context.push(props);
794
795 database
796 .update_profile_tokens(&other.id, other.tokens, other.ips, other.token_context)
797 .await
798 .unwrap();
799
800 return Json(DefaultReturn {
802 success: true,
803 message: "Generated token!".to_string(),
804 payload: Some(token),
805 });
806}
807
808pub async fn update_password_request(
810 jar: CookieJar,
811 Path(id): Path<String>,
812 State(database): State<Database>,
813 Json(props): Json<SetProfilePassword>,
814) -> impl IntoResponse {
815 let auth_user = match jar.get("__Secure-Token") {
817 Some(c) => {
818 let token = c.value_trimmed();
819
820 match database.get_profile_by_unhashed(token).await {
821 Ok(ua) => {
822 if !ua
824 .token_context_from_token(&token)
825 .can_do(TokenPermission::ManageAccount)
826 {
827 return Json(DatabaseError::NotAllowed.to_json());
828 }
829
830 ua
832 }
833 Err(e) => return Json(e.to_json()),
834 }
835 }
836 None => return Json(DatabaseError::NotAllowed.to_json()),
837 };
838
839 let mut is_manager = false;
841 if auth_user.id != id && auth_user.username != id {
842 let group = match database.get_group_by_id(auth_user.group).await {
843 Ok(g) => g,
844 Err(e) => {
845 return Json(DefaultReturn {
846 success: false,
847 message: e.to_string(),
848 payload: None,
849 })
850 }
851 };
852
853 if !group.permissions.check(FinePermission::EDIT_USER) {
854 return Json(DatabaseError::NotAllowed.to_json());
856 } else {
857 is_manager = true;
858 }
859 }
860
861 if auth_user.group == -1 {
865 return Json(DefaultReturn {
867 success: false,
868 message: DatabaseError::NotAllowed.to_string(),
869 payload: None,
870 });
871 }
872
873 if let Err(e) = database
876 .update_profile_password(&id, &props.password, &props.new_password, !is_manager)
877 .await
878 {
879 return Json(e.to_json());
880 }
881
882 Json(DefaultReturn {
884 success: true,
885 message: "Acceptable".to_string(),
886 payload: Some(props.new_password),
887 })
888}
889
890pub async fn update_username_request(
892 jar: CookieJar,
893 Path(id): Path<String>,
894 State(database): State<Database>,
895 Json(props): Json<SetProfileUsername>,
896) -> impl IntoResponse {
897 let auth_user = match jar.get("__Secure-Token") {
899 Some(c) => {
900 let token = c.value_trimmed();
901
902 match database.get_profile_by_unhashed(token).await {
903 Ok(ua) => {
904 if !ua
906 .token_context_from_token(&token)
907 .can_do(TokenPermission::ManageAccount)
908 {
909 return Json(DatabaseError::NotAllowed.to_json());
910 }
911
912 ua
914 }
915 Err(e) => return Json(e.to_json()),
916 }
917 }
918 None => return Json(DatabaseError::NotAllowed.to_json()),
919 };
920
921 if auth_user.id != id && auth_user.username != id {
923 let group = match database.get_group_by_id(auth_user.group).await {
924 Ok(g) => g,
925 Err(e) => {
926 return Json(DefaultReturn {
927 success: false,
928 message: e.to_string(),
929 payload: None,
930 })
931 }
932 };
933
934 if !group.permissions.check(FinePermission::EDIT_USER) {
935 return Json(DatabaseError::NotAllowed.to_json());
937 }
938 }
939
940 if auth_user.group == -1 {
944 return Json(DefaultReturn {
946 success: false,
947 message: DatabaseError::NotAllowed.to_string(),
948 payload: None,
949 });
950 }
951
952 if let Err(e) = database
955 .update_profile_username(&id, &props.password, &props.new_name)
956 .await
957 {
958 return Json(e.to_json());
959 }
960
961 Json(DefaultReturn {
963 success: true,
964 message: "Acceptable".to_string(),
965 payload: Some(props.new_name),
966 })
967}
968
969pub async fn update_metdata_request(
971 jar: CookieJar,
972 Path(id): Path<String>,
973 State(database): State<Database>,
974 Json(props): Json<SetProfileMetadata>,
975) -> impl IntoResponse {
976 let auth_user = match jar.get("__Secure-Token") {
978 Some(c) => {
979 let token = c.value_trimmed();
980
981 match database.get_profile_by_unhashed(token).await {
982 Ok(ua) => {
983 if !ua
985 .token_context_from_token(&token)
986 .can_do(TokenPermission::ManageProfile)
987 {
988 return Json(DatabaseError::NotAllowed.to_json());
989 }
990
991 ua
993 }
994 Err(e) => return Json(e.to_json()),
995 }
996 }
997 None => return Json(DatabaseError::NotAllowed.to_json()),
998 };
999
1000 if auth_user.id != id && auth_user.username != id {
1002 let group = match database.get_group_by_id(auth_user.group).await {
1003 Ok(g) => g,
1004 Err(e) => {
1005 return Json(DefaultReturn {
1006 success: false,
1007 message: e.to_string(),
1008 payload: (),
1009 })
1010 }
1011 };
1012
1013 if !group
1014 .permissions
1015 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1016 {
1017 return Json(DatabaseError::NotAllowed.to_json());
1019 }
1020 }
1021
1022 if auth_user.group == -1 {
1026 return Json(DefaultReturn {
1028 success: false,
1029 message: DatabaseError::NotAllowed.to_string(),
1030 payload: (),
1031 });
1032 }
1033
1034 match database.update_profile_metadata(&id, props.metadata).await {
1036 Ok(_) => Json(DefaultReturn {
1037 success: true,
1038 message: "Acceptable".to_string(),
1039 payload: (),
1040 }),
1041 Err(e) => Json(e.to_json()),
1042 }
1043}
1044
1045pub async fn patch_metdata_request(
1047 jar: CookieJar,
1048 Path(id): Path<String>,
1049 State(database): State<Database>,
1050 Json(props): Json<SetProfileMetadata>,
1051) -> impl IntoResponse {
1052 let auth_user = match jar.get("__Secure-Token") {
1054 Some(c) => {
1055 let token = c.value_trimmed();
1056
1057 match database.get_profile_by_unhashed(token).await {
1058 Ok(ua) => {
1059 if !ua
1061 .token_context_from_token(&token)
1062 .can_do(TokenPermission::ManageProfile)
1063 {
1064 return Json(DatabaseError::NotAllowed.to_json());
1065 }
1066
1067 ua
1069 }
1070 Err(e) => return Json(e.to_json()),
1071 }
1072 }
1073 None => return Json(DatabaseError::NotAllowed.to_json()),
1074 };
1075
1076 let other_user = match database.get_profile(&id).await {
1078 Ok(ua) => ua,
1079 Err(e) => return Json(e.to_json()),
1080 };
1081
1082 if auth_user.id != id && auth_user.username != id {
1084 let group = match database.get_group_by_id(auth_user.group).await {
1085 Ok(g) => g,
1086 Err(e) => {
1087 return Json(DefaultReturn {
1088 success: false,
1089 message: e.to_string(),
1090 payload: (),
1091 })
1092 }
1093 };
1094
1095 if !group
1096 .permissions
1097 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1098 {
1099 return Json(DatabaseError::NotAllowed.to_json());
1101 }
1102 }
1103
1104 if auth_user.group == -1 {
1108 return Json(DefaultReturn {
1110 success: false,
1111 message: DatabaseError::NotAllowed.to_string(),
1112 payload: (),
1113 });
1114 }
1115
1116 let mut metadata = other_user.metadata.clone();
1118
1119 for kv in props.metadata.kv {
1120 metadata.kv.insert(kv.0, kv.1);
1121 }
1122
1123 if props.metadata.policy_consent != metadata.policy_consent {
1124 metadata.policy_consent = props.metadata.policy_consent;
1125 }
1126
1127 match database.update_profile_metadata(&id, metadata).await {
1129 Ok(_) => Json(DefaultReturn {
1130 success: true,
1131 message: "Acceptable".to_string(),
1132 payload: (),
1133 }),
1134 Err(e) => Json(e.to_json()),
1135 }
1136}
1137
1138pub async fn update_badges_request(
1140 jar: CookieJar,
1141 Path(id): Path<String>,
1142 State(database): State<Database>,
1143 Json(props): Json<SetProfileBadges>,
1144) -> impl IntoResponse {
1145 let auth_user = match jar.get("__Secure-Token") {
1147 Some(c) => {
1148 let token = c.value_trimmed();
1149
1150 match database.get_profile_by_unhashed(token).await {
1151 Ok(ua) => {
1152 if !ua
1154 .token_context_from_token(&token)
1155 .can_do(TokenPermission::Moderator)
1156 {
1157 return Json(DatabaseError::NotAllowed.to_json());
1158 }
1159
1160 ua
1162 }
1163 Err(e) => return Json(e.to_json()),
1164 }
1165 }
1166 None => return Json(DatabaseError::NotAllowed.to_json()),
1167 };
1168
1169 let group = match database.get_group_by_id(auth_user.group).await {
1171 Ok(g) => g,
1172 Err(e) => return Json(e.to_json()),
1173 };
1174
1175 if !group
1176 .permissions
1177 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1178 {
1179 return Json(DefaultReturn {
1181 success: false,
1182 message: DatabaseError::NotAllowed.to_string(),
1183 payload: (),
1184 });
1185 }
1186
1187 match database.update_profile_badges(&id, props.badges).await {
1189 Ok(_) => Json(DefaultReturn {
1190 success: true,
1191 message: "Acceptable".to_string(),
1192 payload: (),
1193 }),
1194 Err(e) => Json(e.to_json()),
1195 }
1196}
1197
1198pub async fn update_labels_request(
1200 jar: CookieJar,
1201 Path(id): Path<String>,
1202 State(database): State<Database>,
1203 Json(props): Json<SetProfileLabels>,
1204) -> impl IntoResponse {
1205 let auth_user = match jar.get("__Secure-Token") {
1207 Some(c) => {
1208 let token = c.value_trimmed();
1209
1210 match database.get_profile_by_unhashed(token).await {
1211 Ok(ua) => {
1212 if !ua
1214 .token_context_from_token(&token)
1215 .can_do(TokenPermission::Moderator)
1216 {
1217 return Json(DatabaseError::NotAllowed.to_json());
1218 }
1219
1220 ua
1222 }
1223 Err(e) => return Json(e.to_json()),
1224 }
1225 }
1226 None => return Json(DatabaseError::NotAllowed.to_json()),
1227 };
1228
1229 let group = match database.get_group_by_id(auth_user.group).await {
1231 Ok(g) => g,
1232 Err(e) => return Json(e.to_json()),
1233 };
1234
1235 if !group
1236 .permissions
1237 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1238 {
1239 return Json(DefaultReturn {
1241 success: false,
1242 message: DatabaseError::NotAllowed.to_string(),
1243 payload: (),
1244 });
1245 }
1246
1247 match database.update_profile_labels(&id, props.labels).await {
1249 Ok(_) => Json(DefaultReturn {
1250 success: true,
1251 message: "Acceptable".to_string(),
1252 payload: (),
1253 }),
1254 Err(e) => Json(e.to_json()),
1255 }
1256}
1257
1258pub async fn update_links_request(
1260 jar: CookieJar,
1261 Path(id): Path<String>,
1262 State(database): State<Database>,
1263 Json(props): Json<SetProfileLinks>,
1264) -> impl IntoResponse {
1265 let auth_user = match jar.get("__Secure-Token") {
1267 Some(c) => {
1268 let token = c.value_trimmed();
1269
1270 match database.get_profile_by_unhashed(token).await {
1271 Ok(ua) => {
1272 if !ua
1274 .token_context_from_token(&token)
1275 .can_do(TokenPermission::Moderator)
1276 {
1277 return Json(DatabaseError::NotAllowed.to_json());
1278 }
1279
1280 ua
1282 }
1283 Err(e) => return Json(e.to_json()),
1284 }
1285 }
1286 None => return Json(DatabaseError::NotAllowed.to_json()),
1287 };
1288
1289 let group = match database.get_group_by_id(auth_user.group).await {
1291 Ok(g) => g,
1292 Err(e) => return Json(e.to_json()),
1293 };
1294
1295 if (auth_user.id != id)
1296 && !group
1297 .permissions
1298 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1299 {
1300 return Json(DatabaseError::NotAllowed.to_json());
1301 }
1302
1303 match database.update_profile_links(&id, props.links).await {
1305 Ok(_) => Json(DefaultReturn {
1306 success: true,
1307 message: "Acceptable".to_string(),
1308 payload: (),
1309 }),
1310 Err(e) => Json(e.to_json()),
1311 }
1312}
1313
1314pub async fn update_layout_request(
1316 jar: CookieJar,
1317 Path(id): Path<String>,
1318 State(database): State<Database>,
1319 Json(props): Json<SetProfileLayout>,
1320) -> impl IntoResponse {
1321 let auth_user = match jar.get("__Secure-Token") {
1323 Some(c) => {
1324 let token = c.value_trimmed();
1325
1326 match database.get_profile_by_unhashed(token).await {
1327 Ok(ua) => {
1328 if !ua
1330 .token_context_from_token(&token)
1331 .can_do(TokenPermission::Moderator)
1332 {
1333 return Json(DatabaseError::NotAllowed.to_json());
1334 }
1335
1336 ua
1338 }
1339 Err(e) => return Json(e.to_json()),
1340 }
1341 }
1342 None => return Json(DatabaseError::NotAllowed.to_json()),
1343 };
1344
1345 let group = match database.get_group_by_id(auth_user.group).await {
1347 Ok(g) => g,
1348 Err(e) => return Json(e.to_json()),
1349 };
1350
1351 if (auth_user.id != id)
1352 && !group
1353 .permissions
1354 .check(FinePermission::MANAGE_PROFILE_SETTINGS)
1355 {
1356 return Json(DatabaseError::NotAllowed.to_json());
1357 }
1358
1359 match database.update_profile_layout(&id, props.layout).await {
1361 Ok(_) => Json(DefaultReturn {
1362 success: true,
1363 message: "Acceptable".to_string(),
1364 payload: (),
1365 }),
1366 Err(e) => Json(e.to_json()),
1367 }
1368}
1369
1370#[derive(Serialize)]
1371struct LayoutRenderResult {
1372 pub block: String,
1373 pub tree: String,
1374}
1375
1376pub async fn render_layout_request(Json(props): Json<RenderLayout>) -> impl IntoResponse {
1378 Json(LayoutRenderResult {
1379 block: props.layout.render_block(),
1380 tree: props.layout.render_tree(),
1381 })
1382}
1383
1384pub async fn delete_request(
1386 jar: CookieJar,
1387 Path(id): Path<String>,
1388 State(database): State<Database>,
1389) -> impl IntoResponse {
1390 let auth_user = match jar.get("__Secure-Token") {
1392 Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await {
1393 Ok(ua) => ua,
1394 Err(e) => return Json(e.to_json()),
1395 },
1396 None => return Json(DatabaseError::NotAllowed.to_json()),
1397 };
1398
1399 if auth_user.username != id {
1401 let group = match database.get_group_by_id(auth_user.group).await {
1402 Ok(g) => g,
1403 Err(e) => {
1404 return Json(DefaultReturn {
1405 success: false,
1406 message: e.to_string(),
1407 payload: (),
1408 })
1409 }
1410 };
1411
1412 let other_user = match database.get_profile_by_id(&id).await {
1414 Ok(ua) => ua,
1415 Err(e) => return Json(e.to_json()),
1416 };
1417
1418 if !group.permissions.check(FinePermission::DELETE_USER) {
1419 return Json(DatabaseError::NotAllowed.to_json());
1421 } else {
1422 let actor_id = auth_user.id;
1423 simplify!(
1424 database
1425 .create_notification(
1426 NotificationCreate {
1427 title: format!("[{actor_id}](/+u/{actor_id})"),
1428 content: format!("Deleted a profile: @{}", other_user.username),
1429 address: format!("/+u/{actor_id}"),
1430 recipient: "*(audit)".to_string(), },
1432 None,
1433 )
1434 .await; Err; Json(DatabaseError::Other.to_json())
1435 );
1436 }
1437
1438 let group = match database.get_group_by_id(other_user.group).await {
1440 Ok(g) => g,
1441 Err(e) => {
1442 return Json(DefaultReturn {
1443 success: false,
1444 message: e.to_string(),
1445 payload: (),
1446 })
1447 }
1448 };
1449
1450 if group.permissions.check(FinePermission::DELETE_USER) {
1451 return Json(DatabaseError::NotAllowed.to_json());
1453 }
1454 }
1455
1456 if auth_user.group == -1 {
1460 return Json(DefaultReturn {
1462 success: false,
1463 message: DatabaseError::NotAllowed.to_string(),
1464 payload: (),
1465 });
1466 }
1467
1468 match database.delete_profile_by_id(&id).await {
1470 Ok(_) => Json(DefaultReturn {
1471 success: true,
1472 message: "Acceptable".to_string(),
1473 payload: (),
1474 }),
1475 Err(e) => Json(e.to_json()),
1476 }
1477}
1478
1479pub async fn css_request(
1481 Path(id): Path<String>,
1482 State(database): State<Database>,
1483) -> impl IntoResponse {
1484 let auth_user = match database.get_profile(&id).await {
1486 Ok(ua) => ua,
1487 Err(_) => {
1488 return String::new();
1489 }
1490 };
1491
1492 let mut out: String = format!(
1494 "{}\n*, :root {{\n",
1495 auth_user
1496 .metadata
1497 .soft_get("rainbeam:market_theme_template")
1498 );
1499
1500 for style in auth_user.metadata.kv.clone() {
1501 if !style.0.starts_with("sparkler:color_") {
1502 continue;
1503 }
1504
1505 out.push_str(&format!(
1506 " --{}: {};\n",
1507 style.0.replace("_", "-").replace("sparkler:", ""),
1508 style.1,
1509 ));
1510 }
1511
1512 out.push_str(&auth_user.metadata.soft_get("sparkler:custom_css"));
1513 format!("{out}\n}}")
1514}
1515
1516pub async fn enable_totp_request(
1518 jar: CookieJar,
1519 Path(id): Path<String>,
1520 State(database): State<Database>,
1521) -> impl IntoResponse {
1522 let auth_user = match jar.get("__Secure-Token") {
1524 Some(c) => {
1525 let token = c.value_trimmed();
1526
1527 match database.get_profile_by_unhashed(token).await {
1528 Ok(ua) => {
1529 if !ua
1531 .token_context_from_token(&token)
1532 .can_do(TokenPermission::ManageAccount)
1533 {
1534 return Json(DatabaseError::NotAllowed.to_json());
1535 }
1536
1537 ua
1539 }
1540 Err(e) => return Json(e.to_json()),
1541 }
1542 }
1543 None => return Json(DatabaseError::NotAllowed.to_json()),
1544 };
1545
1546 match database.enable_totp(auth_user, &id).await {
1548 Ok(t) => {
1549 return Json(DefaultReturn {
1550 success: true,
1551 message: "TOTP enabled".to_string(),
1552 payload: Some(t),
1553 })
1554 }
1555 Err(e) => return Json(e.to_json()),
1556 }
1557}
1558
1559pub async fn disable_totp_request(
1561 jar: CookieJar,
1562 Path(id): Path<String>,
1563 State(database): State<Database>,
1564 Json(props): Json<TOTPDisable>,
1565) -> impl IntoResponse {
1566 match jar.get("__Secure-Token") {
1568 Some(c) => {
1569 let token = c.value_trimmed();
1570
1571 match database.get_profile_by_unhashed(token).await {
1572 Ok(ua) => {
1573 if !ua
1575 .token_context_from_token(&token)
1576 .can_do(TokenPermission::ManageAccount)
1577 {
1578 return Json(DatabaseError::NotAllowed.to_json());
1579 }
1580
1581 ua
1583 }
1584 Err(e) => return Json(e.to_json()),
1585 }
1586 }
1587 None => return Json(DatabaseError::NotAllowed.to_json()),
1588 };
1589
1590 let profile = match database.get_profile(&id).await {
1592 Ok(p) => p,
1593 Err(e) => return Json(e.to_json()),
1594 };
1595
1596 if !database.check_totp(&profile, &props.totp) {
1598 return Json(DatabaseError::NotAllowed.to_json());
1599 }
1600
1601 if let Err(e) = database
1603 .update_profile_totp_secret(&profile.id, "", &Vec::new())
1604 .await
1605 {
1606 return Json(e.to_json());
1607 }
1608
1609 Json(DefaultReturn {
1611 success: true,
1612 message: "TOTP disabled".to_string(),
1613 payload: (),
1614 })
1615}
1616
1617pub async fn refresh_totp_recovery_codes_request(
1619 jar: CookieJar,
1620 Path(id): Path<String>,
1621 State(database): State<Database>,
1622 Json(props): Json<TOTPDisable>,
1623) -> impl IntoResponse {
1624 match jar.get("__Secure-Token") {
1626 Some(c) => {
1627 let token = c.value_trimmed();
1628
1629 match database.get_profile_by_unhashed(token).await {
1630 Ok(ua) => {
1631 if !ua
1633 .token_context_from_token(&token)
1634 .can_do(TokenPermission::ManageAccount)
1635 {
1636 return Json(DatabaseError::NotAllowed.to_json());
1637 }
1638
1639 ua
1641 }
1642 Err(e) => return Json(e.to_json()),
1643 }
1644 }
1645 None => return Json(DatabaseError::NotAllowed.to_json()),
1646 };
1647
1648 let profile = match database.get_profile(&id).await {
1650 Ok(p) => p,
1651 Err(e) => return Json(e.to_json()),
1652 };
1653
1654 if !database.check_totp(&profile, &props.totp) {
1656 return Json(DatabaseError::NotAllowed.to_json());
1657 }
1658
1659 let recovery = Database::generate_totp_recovery_codes();
1661
1662 if let Err(e) = database
1663 .update_profile_totp_secret(&profile.id, &profile.totp, &recovery)
1664 .await
1665 {
1666 return Json(e.to_json());
1667 }
1668
1669 Json(DefaultReturn {
1671 success: true,
1672 message: "TOTP disabled".to_string(),
1673 payload: Some(recovery),
1674 })
1675}