1use chrono::{DateTime, NaiveDate, Utc};
4use macros::make_path_parts;
5use serde::{Deserialize, Serialize, Serializer};
6use std::fmt;
7use strum_macros::Display;
8
9use crate::domain::billing::{
10 AccountId, AmountInCents, PlanTier, PlanType, SchoolId, SubscriptionStatus, UserAccountSummary,
11};
12use crate::{
13 api::endpoints::PathPart,
14 domain::{
15 circle::CircleId,
16 image::ImageId,
17 meta::{AffiliationId, AgeRangeId, SubjectId},
18 },
19};
20
21pub mod public_user;
22
23wrap_uuid! {
24 pub struct UserId
26}
27
28#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy)]
32#[non_exhaustive]
33#[repr(i16)]
34pub enum UserScope {
35 Admin = 1,
37
38 ManageCategory = 2,
40
41 ManageImage = 3,
43
44 AdminAsset = 4,
46
47 ManageAnimation = 6,
49
50 ManageEntry = 7,
52
53 ManageSelfAsset = 8,
55
56 ManageAudio = 9,
58
59 Resources = 10,
61}
62
63impl TryFrom<i16> for UserScope {
64 type Error = anyhow::Error;
65
66 fn try_from(i: i16) -> Result<Self, Self::Error> {
67 match i {
68 1 => Ok(Self::Admin),
69 2 => Ok(Self::ManageCategory),
70 3 => Ok(Self::ManageImage),
71 4 => Ok(Self::AdminAsset),
72 6 => Ok(Self::ManageAnimation),
73 7 => Ok(Self::ManageEntry),
74 8 => Ok(Self::ManageSelfAsset),
75 9 => Ok(Self::ManageAudio),
76 10 => Ok(Self::Resources),
77 _ => anyhow::bail!("Scope {} is invalid", i),
78 }
79 }
80}
81
82make_path_parts!(UserLookupPath => "/v1/user/lookup");
83
84#[derive(Debug, Serialize, Deserialize, Clone, Default)]
88pub struct UserLookupQuery {
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub id: Option<UserId>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub name: Option<String>,
96}
97
98#[derive(Debug, Serialize, Deserialize, Clone)]
100pub struct OtherUser {
101 pub id: UserId,
103}
104
105make_path_parts!(ResetEmailPath => "/v1/user/me/reset-email");
106
107#[derive(Debug, Serialize, Deserialize, Clone, Default)]
109pub struct ResetEmailRequest {
110 pub email: String,
112}
113
114#[derive(Debug, Serialize, Deserialize, Clone, Default)]
116pub struct ResetEmailResponse {
117 pub paseto_token: String,
119}
120
121#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
123#[cfg_attr(feature = "backend", derive(sqlx::Type))]
124#[serde(rename_all = "camelCase")]
125#[strum(serialize_all = "camelCase")]
126#[repr(i16)]
127pub enum UserBadge {
128 MasterTeacher = 0,
130 JiTeam = 1,
132 NoBadge = 10,
134}
135
136impl UserBadge {
137 pub fn as_str(&self) -> &'static str {
139 match self {
140 UserBadge::MasterTeacher => "master-teacher",
141 UserBadge::JiTeam => "ji-team",
142 UserBadge::NoBadge => "",
143 }
144 }
145
146 pub fn display_name(&self) -> &'static str {
148 match self {
149 UserBadge::MasterTeacher => "Master Teacher",
150 UserBadge::JiTeam => "JI Team",
151 UserBadge::NoBadge => "",
152 }
153 }
154}
155
156#[derive(Debug, Serialize, Deserialize, Clone)]
158pub struct UserProfile {
159 pub id: UserId,
161
162 pub username: String,
164
165 pub email: String,
167
168 pub is_oauth: bool,
170
171 pub given_name: String,
173
174 pub family_name: String,
176
177 pub profile_image: Option<ImageId>,
179
180 pub language_app: String,
182
183 pub language_emails: String,
185
186 pub languages_spoken: Vec<String>,
188
189 pub opt_into_edu_resources: bool,
191
192 pub over_18: bool,
194
195 pub timezone: chrono_tz::Tz,
197
198 pub bio: String,
200
201 pub badge: Option<UserBadge>,
203
204 #[serde(default)]
206 pub location_public: bool,
207
208 #[serde(default)]
210 pub organization_public: bool, #[serde(default)]
214 pub persona_public: bool, #[serde(default)]
218 pub languages_spoken_public: bool, #[serde(default)]
222 pub bio_public: bool, #[serde(default)]
226 #[serde(skip_serializing_if = "Vec::is_empty")]
227 pub circles: Vec<CircleId>,
228
229 #[serde(default)]
231 #[serde(skip_serializing_if = "Vec::is_empty")]
232 pub scopes: Vec<UserScope>,
233
234 pub created_at: DateTime<Utc>,
236
237 #[serde(default)]
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub updated_at: Option<DateTime<Utc>>,
241
242 #[serde(default)]
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub organization: Option<String>,
246
247 #[serde(default)]
249 #[serde(skip_serializing_if = "Vec::is_empty")]
250 pub persona: Vec<String>,
251
252 #[serde(default)]
254 #[serde(skip_serializing_if = "Vec::is_empty")]
255 pub subjects: Vec<SubjectId>,
256
257 #[serde(default)]
259 #[serde(skip_serializing_if = "Vec::is_empty")]
260 pub age_ranges: Vec<AgeRangeId>,
261
262 #[serde(default)]
264 #[serde(skip_serializing_if = "Vec::is_empty")]
265 pub affiliations: Vec<AffiliationId>,
266
267 #[serde(default)]
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub location: Option<serde_json::Value>,
271
272 #[serde(default)]
274 pub jig_count: u64,
275
276 #[serde(default)]
278 pub resource_count: u64,
279
280 #[serde(default)]
282 pub course_count: u64,
283
284 #[serde(default)]
286 pub playlist_count: u64,
287
288 #[serde(default)]
290 pub total_asset_count: u64,
291
292 #[serde(default)]
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub account_summary: Option<UserAccountSummary>,
299}
300
301impl UserProfile {
302 pub fn school_or_organization(&self) -> Option<&str> {
304 self.account_summary
305 .as_ref()
306 .and_then(|summary| summary.school_name.as_deref())
307 .or(self.organization.as_deref())
308 }
309}
310
311#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
313#[cfg_attr(feature = "backend", derive(sqlx::Type))]
314pub enum UserLoginType {
315 Google,
317 Email,
319}
320
321impl fmt::Display for UserLoginType {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 match self {
324 UserLoginType::Google => write!(f, "Google"),
325 UserLoginType::Email => write!(f, "Email"),
326 }
327 }
328}
329
330#[derive(Debug, Serialize, Deserialize, Clone)]
332#[serde(rename_all = "camelCase")]
333pub struct UserResponse {
334 pub id: UserId,
336
337 pub username: String,
339
340 pub given_name: String,
342
343 pub family_name: String,
345
346 pub email: String,
348
349 #[serde(default)]
351 pub country: Option<String>,
352
353 #[serde(default)]
355 pub state: Option<String>,
356
357 #[serde(default)]
359 pub city: Option<String>,
360
361 #[serde(default)]
363 pub organization: Option<String>,
364
365 pub created_at: NaiveDate,
367
368 pub language: String,
370
371 #[serde(default)]
373 pub badge: Option<UserBadge>,
374
375 #[serde(default)]
377 pub plan_type: Option<PlanType>,
378
379 #[serde(default)]
381 pub subscription_status: Option<SubscriptionStatus>,
382
383 #[serde(default)]
385 pub is_trial: Option<bool>,
386
387 #[serde(default)]
389 pub current_period_end: Option<DateTime<Utc>>,
390
391 #[serde(default)]
393 pub amount_due_in_cents: Option<AmountInCents>,
394
395 #[serde(default)]
397 pub is_admin: Option<bool>,
398
399 #[serde(default)]
401 pub school_id: Option<SchoolId>,
402
403 #[serde(default)]
405 pub school_name: Option<String>,
406
407 #[serde(default)]
409 pub account_id: Option<AccountId>,
410
411 #[serde(default)]
413 pub tier_override: Option<PlanTier>,
414
415 pub login_type: UserLoginType,
417}
418
419#[derive(Debug, Serialize, Deserialize, Clone)]
421pub struct UserProfileExport {
422 pub id: UserId,
424 pub username: String,
426 pub email: String,
428 pub given_name: String,
430 pub family_name: String,
432 pub profile_image: Option<ImageId>,
434 pub last_login: Option<DateTime<Utc>>,
436 pub last_action: Option<DateTime<Utc>>,
438 pub language_app: String,
440 pub language_emails: String,
442 #[serde(default)]
444 #[serde(serialize_with = "serialize_list")]
445 pub languages_spoken: Vec<String>,
446 pub created_at: DateTime<Utc>,
448 #[serde(default)]
450 pub updated_at: Option<DateTime<Utc>>,
451 #[serde(default)]
453 pub organization: Option<String>,
454 #[serde(default)]
456 #[serde(serialize_with = "serialize_list")]
457 pub persona: Vec<String>,
458 #[serde(default)]
460 #[serde(serialize_with = "serialize_list")]
461 pub subjects: Vec<String>,
462 #[serde(default)]
464 #[serde(serialize_with = "serialize_list")]
465 pub age_ranges: Vec<String>,
466 #[serde(default)]
468 #[serde(serialize_with = "serialize_list")]
469 pub affiliations: Vec<String>,
470 #[serde(default)]
472 pub city: Option<String>,
473 #[serde(default)]
475 pub state: Option<String>,
476 #[serde(default)]
478 pub country: Option<String>,
479 pub opt_into_edu_resources: bool,
481 pub liked_jig_count: i64,
483 pub published_jigs_count: i64,
485 }
488
489fn serialize_list<S, T>(list: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
490where
491 S: Serializer,
492 T: Serialize,
493{
494 list.iter()
495 .map(|v| serde_json::to_string(v).unwrap())
496 .collect::<Vec<String>>()
497 .join(", ")
498 .serialize(serializer)
499}
500
501impl UserProfile {
502 pub fn display_name(&self) -> String {
504 format!("{} {}", self.given_name, self.family_name)
505 .trim()
506 .to_string()
507 }
508}
509
510make_path_parts!(VerifyEmailPath => "/v1/user/verify-email");
511
512#[derive(Debug, Serialize, Deserialize, Clone)]
514#[serde(rename_all = "camelCase")]
515pub enum VerifyEmailRequest {
516 Verify {
518 token: String,
520 },
521
522 Resend {
524 email: String,
526 },
527}
528
529make_path_parts!(VerifyResetEmailPath => "/v1/user/verify-reset-email");
530
531#[derive(Debug, Serialize, Deserialize, Clone)]
533#[serde(untagged)]
534pub enum VerifyResetEmailRequest {
535 #[serde(rename_all = "camelCase")]
537 Verify {
538 paseto_token: String,
540
541 force_logout: bool,
543 },
544
545 #[serde(rename_all = "camelCase")]
547 Resend {
548 paseto_token: String,
550 },
551}
552
553make_path_parts!(CreateProfilePath => "/v1/user/me/profile");
554
555#[derive(Debug, Serialize, Deserialize)]
557pub struct CreateProfileRequest {
558 pub username: String,
562
563 pub over_18: bool,
565
566 pub given_name: String,
568
569 pub family_name: String,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
575 pub profile_image_url: Option<String>,
576
577 pub language_app: String,
579
580 pub language_emails: String,
582
583 pub languages_spoken: Vec<String>,
585
586 pub timezone: chrono_tz::Tz,
588
589 pub opt_into_edu_resources: bool,
592
593 #[serde(default)]
595 #[serde(skip_serializing_if = "Option::is_none")]
596 pub organization: Option<String>,
597
598 #[serde(default)]
600 #[serde(skip_serializing_if = "Vec::is_empty")]
601 pub persona: Vec<String>,
602
603 #[serde(default)]
605 #[serde(skip_serializing_if = "Vec::is_empty")]
606 pub subjects: Vec<SubjectId>,
607
608 #[serde(default)]
610 #[serde(skip_serializing_if = "Vec::is_empty")]
611 pub age_ranges: Vec<AgeRangeId>,
612
613 #[serde(default)]
615 #[serde(skip_serializing_if = "Vec::is_empty")]
616 pub affiliations: Vec<AffiliationId>,
617
618 #[serde(default)]
620 #[serde(skip_serializing_if = "Option::is_none")]
621 pub location: Option<serde_json::Value>,
622}
623
624make_path_parts!(GetProfilePath => "/v1/user/me/profile");
625
626make_path_parts!(PatchProfilePath => "/v1/user/me/profile");
627
628#[derive(Debug, Default, Serialize, Deserialize)]
630pub struct PatchProfileRequest {
631 #[serde(default)]
635 #[serde(skip_serializing_if = "Option::is_none")]
636 pub username: Option<String>,
637
638 #[serde(default)]
640 #[serde(skip_serializing_if = "Option::is_none")]
641 pub given_name: Option<String>,
642
643 #[serde(default)]
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub family_name: Option<String>,
647
648 #[serde(default)]
650 #[serde(deserialize_with = "super::deserialize_optional_field")]
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub profile_image: Option<Option<ImageId>>,
653
654 #[serde(default)]
656 #[serde(skip_serializing_if = "Option::is_none")]
657 pub bio: Option<String>,
658
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub language_app: Option<String>,
662
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub language_emails: Option<String>,
666
667 #[serde(default)]
669 #[serde(skip_serializing_if = "Option::is_none")]
670 pub languages_spoken: Option<Vec<String>>,
671
672 #[serde(default)]
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub timezone: Option<chrono_tz::Tz>,
676
677 #[serde(default)]
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub opt_into_edu_resources: Option<bool>,
681
682 #[serde(default)]
684 #[serde(skip_serializing_if = "Option::is_none")]
685 pub organization_public: Option<bool>,
686
687 #[serde(default)]
689 #[serde(skip_serializing_if = "Option::is_none")]
690 pub persona_public: Option<bool>,
691
692 #[serde(default)]
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub languages_spoken_public: Option<bool>,
696
697 #[serde(default)]
699 #[serde(skip_serializing_if = "Option::is_none")]
700 pub location_public: Option<bool>,
701
702 #[serde(default)]
704 #[serde(skip_serializing_if = "Option::is_none")]
705 pub bio_public: Option<bool>,
706
707 #[serde(default)]
711 #[serde(deserialize_with = "super::deserialize_optional_field")]
712 #[serde(skip_serializing_if = "Option::is_none")]
713 pub organization: Option<Option<String>>,
714
715 #[serde(default)]
719 #[serde(skip_serializing_if = "Option::is_none")]
720 pub persona: Option<Vec<String>>,
721
722 #[serde(default)]
726 #[serde(skip_serializing_if = "Option::is_none")]
727 pub subjects: Option<Vec<SubjectId>>,
728
729 #[serde(default)]
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub age_ranges: Option<Vec<AgeRangeId>>,
735
736 #[serde(default)]
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub affiliations: Option<Vec<AffiliationId>>,
742
743 #[serde(default)]
748 #[serde(deserialize_with = "super::deserialize_optional_field")]
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub location: Option<Option<serde_json::Value>>,
751}
752
753make_path_parts!(PatchProfileAdminDataPath => "/v1/user/me/profile/{}/admin-data" => UserId);
754
755#[derive(Debug, Default, Serialize, Deserialize)]
757pub struct PatchProfileAdminDataRequest {
758 #[serde(default)]
760 #[serde(deserialize_with = "super::deserialize_optional_field")]
761 #[serde(skip_serializing_if = "Option::is_none")]
762 pub badge: Option<Option<UserBadge>>,
763}
764
765make_path_parts!(CreateUserPath => "/v1/user");
766
767#[derive(Debug, Serialize, Deserialize)]
769pub struct CreateUserRequest {
770 pub email: String,
772
773 pub password: String,
775}
776
777make_path_parts!(ResetPasswordPath => "/v1/user/password-reset");
778
779#[derive(Debug, Serialize, Deserialize)]
781pub struct ResetPasswordRequest {
782 pub email: String,
784}
785
786make_path_parts!(ChangePasswordPath => "/v1/user/me/password");
787
788#[derive(Debug, Serialize, Deserialize)]
790#[serde(rename_all = "camelCase")]
791pub enum ChangePasswordRequest {
792 Change {
794 token: String,
796
797 password: String,
799
800 force_logout: bool,
802 },
803}
804
805make_path_parts!(UserDeletePath => "/v1/user/me");
806
807make_path_parts!(UserColorCreatePath => "/v1/user/me/color");
810
811make_path_parts!(UserColorUpdatePath => "/v1/user/me/color/{}" => i32);
813
814#[derive(Debug, Serialize, Deserialize)]
816pub struct UserColorValueRequest {
817 pub color: rgb::RGBA8,
819}
820
821make_path_parts!(UserColorGetPath => "/v1/user/me/color");
822
823#[derive(Debug, Serialize, Deserialize)]
825pub struct UserColorResponse {
826 pub colors: Vec<rgb::RGBA8>,
828}
829
830make_path_parts!(UserColorDeletePath => "/v1/user/me/color/{}" => i32);
832
833make_path_parts!(UserFontCreatePath => "/v1/user/me/font");
836
837make_path_parts!(UserFontUpdatePath => "/v1/user/me/font/{}" => i32);
839
840#[derive(Debug, Serialize, Deserialize)]
842pub struct UserFontNameRequest {
843 pub name: String,
845}
846
847make_path_parts!(UserFontGetPath => "/v1/user/me/font");
848
849#[derive(Debug, Serialize, Deserialize)]
851pub struct UserFontResponse {
852 pub names: Vec<String>,
854}
855
856make_path_parts!(UserFontDeletePath => "/v1/user/me/font/{}" => i32);
858
859#[derive(Debug, Serialize, Deserialize, Clone, Default)]
867#[serde(rename_all = "camelCase")]
868pub struct UserBrowseQuery {
869 #[serde(default)]
871 #[serde(skip_serializing_if = "Option::is_none")]
872 pub user_id: Option<UserId>,
873
874 #[serde(default)]
876 #[serde(skip_serializing_if = "Option::is_none")]
877 pub page: Option<u32>,
878
879 #[serde(default)]
881 #[serde(skip_serializing_if = "Option::is_none")]
882 pub page_limit: Option<u32>,
883
884 #[serde(default)]
886 #[serde(deserialize_with = "super::from_csv")]
887 #[serde(skip_serializing_if = "Vec::is_empty")]
888 pub badge: Vec<UserBadge>,
889}
890
891#[derive(Serialize, Deserialize, Clone, Debug)]
893#[serde(rename_all = "camelCase")]
894pub struct UserBrowseResponse {
895 pub users: Vec<UserResponse>,
897
898 pub pages: u32,
900
901 pub total_user_count: u64,
903}
904
905make_path_parts!(UserBrowsePath => "/v1/user/browse");
906
907#[derive(Debug, Serialize, Deserialize, Clone, Default)]
915#[serde(rename_all = "camelCase")]
916pub struct UserSearchQuery {
917 #[serde(default)]
919 #[serde(skip_serializing_if = "String::is_empty")]
920 pub q: String,
921
922 #[serde(default)]
924 #[serde(skip_serializing_if = "Option::is_none")]
925 pub user_id: Option<UserId>,
926
927 #[serde(default)]
929 #[serde(skip_serializing_if = "Option::is_none")]
930 pub page: Option<u32>,
931
932 #[serde(default)]
934 #[serde(skip_serializing_if = "Option::is_none")]
935 pub page_limit: Option<u32>,
936}
937
938#[derive(Serialize, Deserialize, Clone, Debug)]
940#[serde(rename_all = "camelCase")]
941pub struct UserSearchResponse {
942 pub users: Vec<UserResponse>,
944
945 pub pages: u32,
947
948 pub total_user_count: u64,
950}
951
952make_path_parts!(UserSearchPath => "/v1/user");