authbeam/
model.rs

1use hcaptcha_no_wasm::Hcaptcha;
2use std::collections::{BTreeMap, HashMap};
3use totp_rs::TOTP;
4
5use axum::{
6    http::StatusCode,
7    response::{IntoResponse, Response},
8    Json,
9};
10
11use serde::{Serialize, Deserialize};
12use databeam::prelude::DefaultReturn;
13
14use crate::layout::LayoutComponent;
15
16/// Basic user structure
17#[derive(Serialize, Deserialize, Clone, Debug)]
18pub struct Profile {
19    /// User ID.
20    pub id: String,
21    /// User name.
22    pub username: String,
23    /// Hashed user password.
24    pub password: String,
25    /// User password salt.
26    pub salt: String,
27    /// User login tokens.
28    pub tokens: Vec<String>,
29    /// User IPs (these line up with the tokens in `tokens`).
30    pub ips: Vec<String>,
31    /// Extra information about tokens (these line up with the tokens in `tokens`).
32    pub token_context: Vec<TokenContext>,
33    /// Extra user information.
34    pub metadata: ProfileMetadata,
35    /// User badges.
36    ///
37    /// `Vec<(Text, Background, Text Color)>`
38    pub badges: Vec<(String, String, String)>,
39    /// User group.
40    pub group: i32,
41    /// User join timestamp.
42    pub joined: u128,
43    /// User tier for paid benefits.
44    pub tier: i32,
45    /// The labels applied to the user (comma separated when as string with 1 comma at the end which creates an empty label).
46    ///
47    /// `Vec<id>` - ID references a label in the `xlabels` table.
48    pub labels: Vec<i64>,
49    /// User coin balance.
50    pub coins: i32,
51    /// User links.
52    pub links: BTreeMap<String, String>,
53    /// User layout.
54    pub layout: LayoutComponent,
55    /// The number of questions the profile has asked.
56    pub question_count: usize,
57    /// The number of responses the profile has posted.
58    pub response_count: usize,
59    /// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
60    #[serde(default)]
61    pub totp: String,
62    /// The TOTP recovery codes for this profile.
63    #[serde(default)]
64    pub recovery_codes: Vec<String>,
65    /// The number of unread notifications the profile has.
66    pub notification_count: usize,
67    /// The number of unread questions the profile has in their inbox.
68    pub inbox_count: usize,
69}
70
71impl Profile {
72    /// Global user profile
73    pub fn global() -> Self {
74        Self {
75            username: "@".to_string(),
76            id: "@".to_string(),
77            ..Default::default()
78        }
79    }
80
81    /// System profile
82    pub fn system() -> Self {
83        Self {
84            username: "system".to_string(),
85            id: "0".to_string(),
86            ..Default::default()
87        }
88    }
89
90    /// Anonymous user profile
91    pub fn anonymous(tag: String) -> Self {
92        Self {
93            username: "anonymous".to_string(),
94            id: tag,
95            ..Default::default()
96        }
97    }
98
99    /// Get the tag of an anonymous ID
100    ///
101    /// # Returns
102    /// `(is anonymous, tag, username, input)`
103    pub fn anonymous_tag(input: &str) -> (bool, String, String, String) {
104        if (input != "anonymous") && !input.starts_with("anonymous#") {
105            // not anonymous
106            return (false, String::new(), String::new(), input.to_string());
107        }
108
109        // anonymous questions from BEFORE the anonymous tag update will just have the "anonymous" tag
110        let split: Vec<&str> = input.split("#").collect();
111        (
112            true,
113            split.get(1).unwrap_or(&"unknown").to_string(),
114            split.get(0).unwrap().to_string(),
115            input.to_string(),
116        )
117    }
118
119    /// Clean profile information
120    pub fn clean(&mut self) -> () {
121        self.ips = Vec::new();
122        self.tokens = Vec::new();
123        self.token_context = Vec::new();
124        self.salt = String::new();
125        self.password = String::new();
126        self.metadata = ProfileMetadata::default();
127        self.totp = String::new();
128        self.recovery_codes = Vec::new();
129    }
130
131    /// Get context from a token
132    pub fn token_context_from_token(&self, token: &str) -> TokenContext {
133        let token = databeam::utility::hash(token.to_string());
134
135        if let Some(pos) = self.tokens.iter().position(|t| *t == token) {
136            if let Some(ctx) = self.token_context.get(pos) {
137                return ctx.to_owned();
138            }
139
140            return TokenContext::default();
141        }
142
143        return TokenContext::default();
144    }
145
146    // labels
147
148    /// Check if the user has the given label by `id`.
149    pub fn has_label(&self, id: i64) -> bool {
150        self.labels.contains(&id)
151    }
152
153    // totp
154
155    /// Get a [`TOTP`] from the profile's `totp` secret value.
156    pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
157        if self.totp.is_empty() {
158            return None;
159        }
160
161        match TOTP::new(
162            totp_rs::Algorithm::SHA1,
163            6,
164            1,
165            30,
166            self.totp.as_bytes().to_owned(),
167            Some(issuer.unwrap_or("Rainbeam".to_string())),
168            self.username.clone(),
169        ) {
170            Ok(t) => Some(t),
171            Err(_) => None,
172        }
173    }
174}
175
176impl Default for Profile {
177    fn default() -> Self {
178        Self {
179            id: String::new(),
180            username: String::new(),
181            password: String::new(),
182            salt: String::new(),
183            tokens: Vec::new(),
184            ips: Vec::new(),
185            token_context: Vec::new(),
186            metadata: ProfileMetadata::default(),
187            badges: Vec::new(),
188            group: 0,
189            joined: databeam::utility::unix_epoch_timestamp(),
190            tier: 0,
191            labels: Vec::new(),
192            coins: 0,
193            links: BTreeMap::new(),
194            layout: LayoutComponent::default(),
195            question_count: 0,
196            response_count: 0,
197            totp: String::new(),
198            recovery_codes: Vec::new(),
199            notification_count: 0,
200            inbox_count: 0,
201        }
202    }
203}
204
205#[derive(Serialize, Deserialize, Clone, Debug)]
206pub struct TokenContext {
207    #[serde(default)]
208    pub app: Option<String>,
209    #[serde(default)]
210    pub permissions: Option<Vec<TokenPermission>>,
211    #[serde(default)]
212    pub timestamp: u128,
213}
214
215#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
216pub enum TokenPermission {
217    /// Manage UGC (user-generated-content) uploaded by the user
218    ManageAssets,
219    /// Manage user metadata
220    ManageProfile,
221    /// Manage all user fields
222    ManageAccount,
223    /// Execute moderator actions
224    Moderator,
225    /// Generate tokens on behalf of the account
226    ///
227    /// Generated tokens cannot have any permissions the token used to generate it doesn't have
228    GenerateTokens,
229}
230
231impl TokenContext {
232    /// Get the value of the token's `app` field
233    ///
234    /// Returns an empty string if the field value is `None`
235    pub fn app_name(&self) -> String {
236        if let Some(ref name) = self.app {
237            return name.to_string();
238        }
239
240        String::new()
241    }
242
243    /// Check if the token has the given [`TokenPermission`]
244    ///
245    /// ### Returns `true` if the field value is `None`
246    pub fn can_do(&self, permission: TokenPermission) -> bool {
247        if let Some(ref permissions) = self.permissions {
248            return permissions.contains(&permission);
249        }
250
251        return true;
252    }
253}
254
255impl Default for TokenContext {
256    fn default() -> Self {
257        Self {
258            app: None,
259            permissions: None,
260            timestamp: databeam::utility::unix_epoch_timestamp(),
261        }
262    }
263}
264
265#[derive(Serialize, Deserialize, Clone, Debug)]
266pub struct ProfileMetadata {
267    #[serde(default)]
268    pub email: String,
269    #[serde(default)]
270    pub policy_consent: bool,
271    /// Extra key-value pairs
272    #[serde(default)]
273    pub kv: HashMap<String, String>,
274}
275
276impl ProfileMetadata {
277    /// Check if a value exists in `kv` (and isn't empty)
278    pub fn exists(&self, key: &str) -> bool {
279        if let Some(ref value) = self.kv.get(key) {
280            if value.is_empty() {
281                return false;
282            }
283
284            return true;
285        }
286
287        false
288    }
289
290    /// Check if a value in `kv` is "true"
291    pub fn is_true(&self, key: &str) -> bool {
292        if !self.exists(key) {
293            return false;
294        }
295
296        self.kv.get(key).unwrap() == "true"
297    }
298
299    /// Get a value from `kv`, returns an empty string if it doesn't exist
300    pub fn soft_get(&self, key: &str) -> String {
301        if !self.exists(key) {
302            return String::new();
303        }
304
305        self.kv.get(key).unwrap().to_owned()
306    }
307
308    /// Check `kv` lengths
309    ///
310    /// # Returns
311    /// * `true`: ok
312    /// * `false`: invalid
313    pub fn check(&self) -> bool {
314        for field in &self.kv {
315            if field.0 == "sparkler:custom_css" {
316                // custom_css gets an extra long value
317                if field.1.len() > 64 * 128 {
318                    return false;
319                }
320
321                continue;
322            }
323
324            if field.1.len() > 64 * 64 {
325                return false;
326            }
327        }
328
329        true
330    }
331}
332
333impl ProfileMetadata {
334    pub fn from_email(email: String) -> Self {
335        Self {
336            email,
337            policy_consent: true,
338            kv: HashMap::new(),
339        }
340    }
341}
342
343impl Default for ProfileMetadata {
344    fn default() -> Self {
345        Self {
346            email: String::new(),
347            policy_consent: true, // we can mark this as true since it is required for sign up
348            kv: HashMap::new(),
349        }
350    }
351}
352
353/// Basic follow structure
354#[derive(Serialize, Deserialize, Clone, Debug)]
355pub struct UserFollow {
356    /// The ID of the user following
357    pub user: String,
358    /// The ID of the user they are following
359    pub following: String,
360}
361
362/// Basic notification structure
363#[derive(Serialize, Deserialize, Clone, Debug)]
364pub struct Notification {
365    /// The title of the notification
366    pub title: String,
367    /// The content of the notification
368    pub content: String,
369    /// The address of the notification (where it goes)
370    pub address: String,
371    /// The timestamp of when the notification was created
372    pub timestamp: u128,
373    /// The ID of the notification
374    pub id: String,
375    /// The recipient of the notification
376    pub recipient: String,
377}
378
379/// Basic warning structure
380#[derive(Serialize, Deserialize, Clone, Debug)]
381pub struct Warning {
382    /// The ID of the warning
383    pub id: String,
384    /// The content of the warning
385    pub content: String,
386    /// The timestamp of when the warning was created
387    pub timestamp: u128,
388    /// The recipient of the warning
389    pub recipient: String,
390    /// The moderator who warned the recipient
391    pub moderator: Box<Profile>,
392}
393
394/// Basic IP ban
395#[derive(Serialize, Deserialize, Clone, Debug)]
396pub struct IpBan {
397    /// The ID of the ban
398    pub id: String,
399    /// The IP that was banned
400    pub ip: String,
401    /// The reason for the ban
402    pub reason: String,
403    /// The user that banned this IP
404    pub moderator: Box<Profile>,
405    /// The timestamp of when the ban was created
406    pub timestamp: u128,
407}
408
409/// The state of a user's relationship with another user
410#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
411pub enum RelationshipStatus {
412    /// No relationship
413    Unknown,
414    /// User two is blocked from interacting with user one
415    Blocked,
416    /// User two is pending a friend request from user one
417    Pending,
418    /// User two is friends with user one
419    Friends,
420}
421
422impl Default for RelationshipStatus {
423    fn default() -> Self {
424        Self::Unknown
425    }
426}
427
428/// A user's relationship with another user
429///
430/// If a relationship already exists, user two cannot attempt to create a relationship with user one.
431/// The existing relation should be used.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct Relationship {
434    /// The first user in the relationship
435    pub one: Profile,
436    /// The second user in the relationship
437    pub two: Profile,
438    /// The status of the relationship
439    pub status: RelationshipStatus,
440    /// The timestamp of the relationship's creation
441    pub timestamp: u128,
442}
443
444/// An IP-based block
445#[derive(Serialize, Deserialize, Clone, Debug)]
446pub struct IpBlock {
447    /// The ID of the block
448    pub id: String,
449    /// The IP that was blocked
450    pub ip: String,
451    /// The user that blocked this IP
452    pub user: String,
453    /// The context of this block (question content, etc.)
454    pub context: String,
455    /// The timestamp of when the block was created
456    pub timestamp: u128,
457}
458
459pub use crate::permissions::FinePermission;
460
461/// Basic permission group
462#[derive(Serialize, Deserialize, Clone, Debug)]
463pub struct Group {
464    pub name: String,
465    pub id: i32,
466    pub permissions: FinePermission,
467}
468
469impl Default for Group {
470    fn default() -> Self {
471        Self {
472            name: "default".to_string(),
473            id: 0,
474            permissions: FinePermission::default(),
475        }
476    }
477}
478
479pub const RESERVED_LABEL_QUARANTINE: i64 = -1;
480
481/// A label which describes a user
482#[derive(Serialize, Deserialize, Clone, Debug)]
483pub struct UserLabel {
484    /// The ID of the label (unique)
485    pub id: i64,
486    /// The name of the label
487    pub name: String,
488    /// The timestamp of when the label was created
489    pub timestamp: u128,
490    /// The ID creator of the label
491    pub creator: String,
492}
493
494/// A coin transaction between two users
495#[derive(Serialize, Deserialize, Clone, Debug)]
496pub struct Transaction {
497    /// The ID of the transaction (unique)
498    pub id: String,
499    /// The amount of the transaction
500    pub amount: i32,
501    /// The ID of the item purchased
502    pub item: String,
503    /// The timestamp of when the transaction was created
504    pub timestamp: u128,
505    /// The ID of the customer (who bought the item)
506    pub customer: String,
507    /// The ID of the merchant (who sold the item)
508    pub merchant: String,
509}
510
511/// A marketplace item type
512#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
513pub enum ItemType {
514    #[serde(alias = "text")]
515    Text,
516    #[serde(alias = "usertheme")]
517    UserTheme,
518    #[serde(alias = "module")]
519    Module,
520    #[serde(alias = "layout")]
521    Layout,
522}
523
524impl Default for ItemType {
525    fn default() -> Self {
526        Self::Text
527    }
528}
529
530impl ToString for ItemType {
531    fn to_string(&self) -> String {
532        // match self {
533        //     ItemType::Text => "Text".to_string(),
534        //     ItemType::UserTheme => "UserTheme".to_string(),
535        //     ItemType::Module => "Module".to_string(),
536        //     ItemType::Layout => "Layout".to_string(),
537        // }
538        format!("{:?}", self)
539    }
540}
541
542/// A marketplace item status
543#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
544pub enum ItemStatus {
545    /// The item has been reviewed by a site moderator and rejected
546    Rejected,
547    /// The item has not been approved by a site moderator
548    Pending,
549    /// The item has been approved by a site moderator
550    Approved,
551    /// The item has been featured by a site moderator
552    Featured,
553}
554
555impl Default for ItemStatus {
556    fn default() -> Self {
557        Self::Approved
558    }
559}
560
561impl ToString for ItemStatus {
562    fn to_string(&self) -> String {
563        match self {
564            ItemStatus::Rejected => "Rejected".to_string(),
565            ItemStatus::Pending => "Pending".to_string(),
566            ItemStatus::Approved => "Approved".to_string(),
567            ItemStatus::Featured => "Featured".to_string(),
568        }
569    }
570}
571
572/// A marketplace item (for [`Transaction`]s)
573#[derive(Serialize, Deserialize, Clone, Debug)]
574pub struct Item {
575    /// The ID of the item (unique)
576    pub id: String,
577    /// The name of this item
578    pub name: String,
579    /// The description of this item
580    pub description: String,
581    /// The number of user coins this item costs
582    ///
583    /// 0: free
584    /// -1: off-sale
585    pub cost: i32,
586    /// The content of this item
587    pub content: String,
588    /// The type of this item
589    pub r#type: ItemType,
590    /// The status of this item
591    pub status: ItemStatus,
592    /// The timestamp of when the item was created
593    pub timestamp: u128,
594    /// The ID of the item creator
595    pub creator: String,
596}
597
598// props
599#[derive(Serialize, Deserialize, Debug, Hcaptcha)]
600pub struct ProfileCreate {
601    pub username: String,
602    pub password: String,
603    pub policy_consent: bool,
604    #[captcha]
605    pub token: String,
606}
607
608#[derive(Serialize, Deserialize, Debug, Hcaptcha)]
609pub struct ProfileLogin {
610    pub username: String,
611    pub password: String,
612    #[captcha]
613    pub token: String,
614    #[serde(default)]
615    pub totp: String,
616}
617
618#[derive(Serialize, Deserialize, Debug)]
619pub struct SetProfileMetadata {
620    pub metadata: ProfileMetadata,
621}
622
623#[derive(Serialize, Deserialize, Debug)]
624pub struct SetProfileBadges {
625    pub badges: Vec<(String, String, String)>,
626}
627
628#[derive(Serialize, Deserialize, Debug)]
629pub struct SetProfileLabels {
630    pub labels: Vec<i64>,
631}
632
633#[derive(Serialize, Deserialize, Debug)]
634pub struct SetProfileLinks {
635    pub links: BTreeMap<String, String>,
636}
637
638#[derive(Serialize, Deserialize, Debug)]
639pub struct SetProfileLayout {
640    pub layout: LayoutComponent,
641}
642
643#[derive(Serialize, Deserialize, Debug)]
644pub struct RenderLayout {
645    pub layout: LayoutComponent,
646}
647
648#[derive(Serialize, Deserialize, Debug)]
649pub struct SetProfileGroup {
650    pub group: i32,
651}
652
653#[derive(Serialize, Deserialize, Debug)]
654pub struct SetProfileTier {
655    pub tier: i32,
656}
657
658#[derive(Serialize, Deserialize, Debug)]
659pub struct SetProfileCoins {
660    pub coins: i32,
661}
662
663#[derive(Serialize, Deserialize, Debug)]
664pub struct SetProfilePassword {
665    pub password: String,
666    pub new_password: String,
667}
668
669#[derive(Serialize, Deserialize, Debug)]
670pub struct SetProfileUsername {
671    pub password: String,
672    pub new_name: String,
673}
674
675#[derive(Serialize, Deserialize, Debug)]
676pub struct NotificationCreate {
677    pub title: String,
678    pub content: String,
679    pub address: String,
680    pub recipient: String,
681}
682
683#[derive(Serialize, Deserialize, Debug)]
684pub struct WarningCreate {
685    pub content: String,
686    pub recipient: String,
687}
688
689#[derive(Serialize, Deserialize, Debug)]
690pub struct IpBanCreate {
691    pub ip: String,
692    pub reason: String,
693}
694
695#[derive(Serialize, Deserialize, Debug)]
696pub struct IpBlockCreate {
697    pub ip: String,
698    pub context: String,
699}
700
701#[derive(Serialize, Deserialize, Debug)]
702pub struct TransactionCreate {
703    // pub customer: String,
704    pub merchant: String,
705    pub item: String,
706    pub amount: i32,
707}
708
709#[derive(Serialize, Deserialize, Debug)]
710pub struct ItemCreate {
711    pub name: String,
712    pub description: String,
713    pub content: String,
714    pub cost: i32,
715    pub r#type: ItemType,
716}
717
718#[derive(Serialize, Deserialize, Debug)]
719pub struct ItemEdit {
720    pub name: String,
721    pub description: String,
722    pub cost: i32,
723}
724
725#[derive(Serialize, Deserialize, Debug)]
726pub struct ItemEditContent {
727    pub content: String,
728}
729
730#[derive(Serialize, Deserialize, Debug)]
731pub struct SetItemStatus {
732    pub status: ItemStatus,
733}
734
735#[derive(Serialize, Deserialize, Debug)]
736pub struct TOTPDisable {
737    pub totp: String,
738}
739
740#[derive(Serialize, Deserialize, Debug)]
741pub struct LabelCreate {
742    pub id: i64,
743    pub name: String,
744}
745
746/// General API errors
747#[derive(Debug, PartialEq, Eq)]
748pub enum DatabaseError {
749    ModulesMustBeOffsale,
750    IncorrectPassword,
751    UsernameTaken,
752    TooExpensive,
753    MustBeUnique,
754    OutOfScope,
755    NotAllowed,
756    ValueError,
757    NotFound,
758    TooLong,
759    Other,
760}
761
762impl DatabaseError {
763    pub fn to_string(&self) -> String {
764        use DatabaseError::*;
765        match self {
766            ModulesMustBeOffsale => String::from("Modules must be off-sale."),
767            IncorrectPassword => String::from("Given password is incorrect."),
768            UsernameTaken => String::from("This username is already in use."),
769            TooExpensive => String::from("You cannot afford to do this."),
770            MustBeUnique => String::from("One of the given values must be unique."),
771            OutOfScope => String::from(
772                "Cannot generate tokens with permissions the provided token doesn't have.",
773            ),
774            NotAllowed => String::from("You are not allowed to access this resource."),
775            ValueError => String::from("One of the field values given is invalid."),
776            NotFound => String::from("No asset with this ID could be found."),
777            TooLong => String::from("Given data is too long."),
778            _ => String::from("An unspecified error has occured"),
779        }
780    }
781
782    pub fn to_json<T: Default>(&self) -> DefaultReturn<T> {
783        DefaultReturn {
784            success: false,
785            message: self.to_string(),
786            payload: T::default(),
787        }
788    }
789}
790
791impl IntoResponse for DatabaseError {
792    fn into_response(self) -> Response {
793        use crate::model::DatabaseError::*;
794        match self {
795            NotAllowed => (
796                StatusCode::UNAUTHORIZED,
797                Json(DefaultReturn::<u16> {
798                    success: false,
799                    message: self.to_string(),
800                    payload: 401,
801                }),
802            )
803                .into_response(),
804            NotFound => (
805                StatusCode::NOT_FOUND,
806                Json(DefaultReturn::<u16> {
807                    success: false,
808                    message: self.to_string(),
809                    payload: 404,
810                }),
811            )
812                .into_response(),
813            _ => (
814                StatusCode::INTERNAL_SERVER_ERROR,
815                Json(DefaultReturn::<u16> {
816                    success: false,
817                    message: self.to_string(),
818                    payload: 500,
819                }),
820            )
821                .into_response(),
822        }
823    }
824}