Skip to main content

autter_core/
model.rs

1use serde::{Deserialize, Serialize};
2use serde_valid::Validate;
3pub use tetratto_core::model::{ApiReturn, Error, Result};
4use tetratto_shared::{
5    hash::{hash_salted, salt},
6    snow::Snowflake,
7    unix_epoch_timestamp,
8};
9use totp_rs::TOTP;
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
12pub enum ThemePreference {
13    #[default]
14    Auto,
15    Light,
16    Dark,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
20pub enum UserPermission {
21    /// Manage other users.
22    ManageUsers,
23    /// Verify and unverify users.
24    ManageVerified,
25    /// Manage audit log entries.
26    ManageAuditLog,
27    /// Manage reports.
28    ManageReports,
29    /// Manage miscellaneous app data (e.g. Askall asks).
30    ManageMiscAppData,
31    /// Manage user notifications.
32    ManageNotifications,
33    /// User is banned.
34    Banned,
35    /// Ban and unban users.
36    ManageBans,
37    /// Delete other users.
38    DeleteUsers,
39    /// Manage user warnings.
40    ManageWarnings,
41    /// User is a premium user.
42    Seedling,
43    /// User is a premium plus user.
44    SeedlingPlus,
45    /// Manage user properties.
46    ManageProperties,
47    /// Manage user organizations.
48    ManageOrganizations,
49    /// Manage organization permission lists.
50    ManagePermissionsLists,
51    /// Manage the permissions of other users.
52    Administrator,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
56pub enum UserBadge {
57    /// User is a site moderator.
58    Staff,
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize, Validate, Default)]
62pub struct UserSettings {
63    #[serde(default)]
64    #[validate(max_length = 32)]
65    pub display_name: String,
66    #[serde(default)]
67    #[validate(max_length = 4096)]
68    pub biography: String,
69    /// External links for the user's other profiles on other websites.
70    #[serde(default)]
71    #[validate(max_items = 15)]
72    #[validate(unique_items)]
73    pub links: Vec<(String, String)>,
74    /// The user's contacts.
75    #[serde(default)]
76    #[validate(max_items = 5)]
77    #[validate(unique_items)]
78    pub contacts: Vec<(String, String)>,
79    /// The user's theme used on their profile.
80    #[serde(default)]
81    pub profile_theme: ThemePreference,
82}
83
84impl UserSettings {
85    pub fn verify_values(&self) -> Result<()> {
86        if let Err(e) = self.validate() {
87            return Err(Error::MiscError(e.to_string()));
88        }
89
90        Ok(())
91    }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct TetrattoLinkedAccount {
96    pub id: usize,
97    pub username: String,
98    pub added: usize,
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize, Default)]
102pub struct UserLinkedAccounts {
103    #[serde(default)]
104    pub tetratto_multiple: Vec<TetrattoLinkedAccount>,
105}
106
107/// `(ip, token, creation timestamp)`
108pub type Token = (String, String, usize);
109
110#[derive(Clone, Debug, Serialize, Deserialize)]
111pub struct User {
112    pub id: usize,
113    pub created: usize,
114    pub username: String,
115    pub password: String,
116    pub salt: String,
117    pub settings: UserSettings,
118    pub tokens: Vec<Token>,
119    pub permissions: Vec<UserPermission>,
120    pub is_verified: bool,
121    pub notification_count: usize,
122    /// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
123    #[serde(default)]
124    pub totp: String,
125    /// The TOTP recovery codes for this profile.
126    #[serde(default)]
127    pub recovery_codes: Vec<String>,
128    /// The user's Stripe customer ID.
129    #[serde(default)]
130    pub stripe_id: String,
131    /// The reason the user was banned.
132    #[serde(default)]
133    pub ban_reason: String,
134    /// The time at which the user's ban will automatically expire.
135    #[serde(default)]
136    pub ban_expire: usize,
137    /// If the user is deactivated. Deactivated users act almost like deleted
138    /// users, but their data is not wiped.
139    #[serde(default)]
140    pub is_deactivated: bool,
141    /// The IDs of Stripe checkout sessions that this user has successfully completed.
142    ///
143    /// This should be checked BEFORE applying purchases to ensure that the user hasn't
144    /// already applied this purchase.
145    #[serde(default)]
146    pub checkouts: Vec<String>,
147    /// The time in which the user last consented to the site's policies.
148    #[serde(default)]
149    pub last_policy_consent: usize,
150    /// The other accounts in the database that are linked to this account.
151    #[serde(default)]
152    pub linked_accounts: UserLinkedAccounts,
153    /// A list of the user's profile badges.
154    #[serde(default)]
155    pub badges: Vec<UserBadge>,
156    /// The ID of the user's principal organization (i.e. the organization shown on their profile).
157    #[serde(default)]
158    pub principal_org: usize,
159    /// If the user is locked into their principal organization. Their organization's
160    /// owner can manage their account if this is true.
161    #[serde(default)]
162    pub org_as_tenant: bool,
163    /// The number of organizations the user can create.
164    #[serde(default)]
165    pub org_creation_credits: i32,
166    /// The number of users the user can register to an organization.
167    #[serde(default)]
168    pub org_user_register_credits: i32,
169    /// Users can pay to have their account marked as fully verified. This verification
170    /// confirms they are a real human (to a degree). Promoting users who are super
171    /// verified is a good way to keep AI and disgraceful content down.
172    #[serde(default)]
173    pub is_super_verified: bool,
174}
175
176impl Default for User {
177    fn default() -> Self {
178        Self::new("<unknown>".to_string(), String::new())
179    }
180}
181
182impl User {
183    /// Create a new [`User`].
184    pub fn new(username: String, password: String) -> Self {
185        let salt = salt();
186        let password = hash_salted(password, salt.clone());
187        let created = unix_epoch_timestamp();
188
189        Self {
190            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
191            created,
192            username,
193            password,
194            salt,
195            settings: UserSettings::default(),
196            tokens: Vec::new(),
197            permissions: Vec::new(),
198            is_verified: false,
199            notification_count: 0,
200            totp: String::new(),
201            recovery_codes: Vec::new(),
202            stripe_id: String::new(),
203            ban_reason: String::new(),
204            is_deactivated: false,
205            ban_expire: 0,
206            checkouts: Vec::new(),
207            last_policy_consent: created,
208            linked_accounts: UserLinkedAccounts::default(),
209            badges: Vec::new(),
210            principal_org: 0,
211            org_as_tenant: false,
212            org_creation_credits: 0,
213            org_user_register_credits: 0,
214            is_super_verified: false,
215        }
216    }
217
218    /// Deleted user profile.
219    pub fn deleted() -> Self {
220        Self {
221            username: "<deleted>".to_string(),
222            id: 0,
223            ..Default::default()
224        }
225    }
226
227    /// Banned user profile.
228    pub fn banned() -> Self {
229        Self {
230            username: "<banned>".to_string(),
231            id: 0,
232            permissions: vec![UserPermission::Banned],
233            ..Default::default()
234        }
235    }
236
237    /// Create a new token
238    ///
239    /// # Returns
240    /// `(unhashed id, token)`
241    pub fn create_token(ip: &str) -> (String, Token) {
242        let unhashed = tetratto_shared::hash::uuid();
243        (
244            unhashed.clone(),
245            (
246                ip.to_string(),
247                tetratto_shared::hash::hash(unhashed),
248                unix_epoch_timestamp(),
249            ),
250        )
251    }
252
253    /// Check if the given password is correct for the user.
254    pub fn check_password(&self, against: String) -> bool {
255        self.password == hash_salted(against, self.salt.clone())
256    }
257
258    /// Get a [`TOTP`] from the profile's `totp` secret value.
259    pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
260        if self.totp.is_empty() {
261            return None;
262        }
263
264        TOTP::new(
265            totp_rs::Algorithm::SHA1,
266            6,
267            1,
268            30,
269            self.totp.as_bytes().to_owned(),
270            Some(issuer.unwrap_or("tetratto!".to_string())),
271            self.username.clone(),
272        )
273        .ok()
274    }
275
276    /// Clean the struct for public viewing.
277    pub fn clean(&mut self) {
278        self.password = String::new();
279        self.salt = String::new();
280
281        self.tokens = Vec::new();
282
283        self.recovery_codes = Vec::new();
284        self.totp = String::new();
285
286        self.settings = UserSettings::default();
287        self.stripe_id = String::new();
288    }
289}
290
291#[derive(Debug, Serialize, Deserialize)]
292pub struct Notification {
293    pub id: usize,
294    pub created: usize,
295    pub title: String,
296    pub content: String,
297    pub owner: usize,
298    pub read: bool,
299    pub tag: String,
300}
301
302impl Notification {
303    /// Create a new [`Notification`].
304    pub fn new(title: String, content: String, owner: usize) -> Self {
305        Self {
306            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
307            created: unix_epoch_timestamp(),
308            title,
309            content,
310            owner,
311            read: false,
312            tag: String::new(),
313        }
314    }
315}
316
317#[derive(Serialize, Deserialize)]
318pub struct AuditLogEntry {
319    pub id: usize,
320    pub created: usize,
321    pub moderator: usize,
322    pub content: String,
323}
324
325impl AuditLogEntry {
326    /// Create a new [`AuditLogEntry`].
327    pub fn new(moderator: usize, content: String) -> Self {
328        Self {
329            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
330            created: unix_epoch_timestamp(),
331            moderator,
332            content,
333        }
334    }
335}
336
337#[derive(Serialize, Deserialize)]
338pub struct IpBan {
339    pub ip: String,
340    pub created: usize,
341    pub reason: String,
342    pub moderator: usize,
343}
344
345impl IpBan {
346    /// Create a new [`IpBan`].
347    pub fn new(ip: String, moderator: usize, reason: String) -> Self {
348        Self {
349            ip,
350            created: unix_epoch_timestamp(),
351            reason,
352            moderator,
353        }
354    }
355}
356
357#[derive(Serialize, Deserialize)]
358pub struct UserWarning {
359    pub id: usize,
360    pub created: usize,
361    pub receiver: usize,
362    pub moderator: usize,
363    pub content: String,
364}
365
366impl UserWarning {
367    /// Create a new [`UserWarning`].
368    pub fn new(user: usize, moderator: usize, content: String) -> Self {
369        Self {
370            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
371            created: unix_epoch_timestamp(),
372            receiver: user,
373            moderator,
374            content,
375        }
376    }
377}
378
379// socket
380pub mod socket {
381    use serde::{Deserialize, Serialize, de::DeserializeOwned};
382
383    #[derive(Serialize, Deserialize, PartialEq, Eq)]
384    pub enum CrudMessageType {
385        Create,
386        Delete,
387    }
388
389    #[derive(Serialize, Deserialize, PartialEq, Eq)]
390    pub enum PacketType {
391        /// A regular check to ensure the connection is still alive.
392        Ping,
393        /// General text which can be ignored.
394        Text,
395        /// A CRUD operation.
396        Crud(CrudMessageType),
397        /// A text key which identifies the socket.
398        Key,
399    }
400
401    #[derive(Serialize, Deserialize, PartialEq, Eq)]
402    pub enum SocketMethod {
403        /// Authentication and channel identification.
404        Headers,
405        /// A message was sent in the channel.
406        Message,
407        /// A message was deleted in the channel.
408        Delete,
409        /// Forward message from server to client. (Redis pubsub to ws)
410        Forward(PacketType),
411        /// A general packet from client to server. (ws to Redis pubsub)
412        Misc(PacketType),
413        /// A general packet from client to server. (ws to Redis pubsub)
414        Packet(PacketType),
415    }
416
417    #[derive(Serialize, Deserialize)]
418    pub struct SocketMessage {
419        pub method: SocketMethod,
420        pub data: String,
421    }
422
423    impl SocketMessage {
424        pub fn data<T: DeserializeOwned>(&self) -> T {
425            serde_json::from_str(&self.data).unwrap()
426        }
427    }
428
429    /// [`PacketType::Text`]
430    #[derive(Serialize, Deserialize, PartialEq, Eq)]
431    pub struct TextMessage {
432        pub text: String,
433    }
434}
435
436// properties
437pub mod properties {
438    use serde::{Deserialize, Serialize};
439    use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
440
441    #[derive(Serialize, Deserialize, PartialEq, Eq)]
442    pub enum Product {
443        /// A single Tetratto account.
444        Tetratto,
445        /// Ability to create a Shrimpcamp organization.
446        ///
447        /// $200 once.
448        Organization,
449        /// Ability to register a user to an organization.
450        ///
451        /// $4 once.
452        UserReg,
453        /// A seedling membership.
454        ///
455        /// $4 monthly.
456        Seedling,
457    }
458
459    #[derive(Serialize, Deserialize)]
460    pub struct Property {
461        pub id: usize,
462        pub created: usize,
463        pub owner: usize,
464        /// A unique name for the instance. Used in the subdomain for the CNAME
465        /// record's host (aka the URL for when the instance doesn't have a custom domain).
466        pub name: String,
467        pub product: Product,
468        pub custom_domain: String,
469    }
470
471    impl Property {
472        /// Create a new [`Property`].
473        pub fn new(owner: usize, name: String, product: Product) -> Self {
474            Self {
475                id: Snowflake::new().to_string().parse::<usize>().unwrap(),
476                created: unix_epoch_timestamp(),
477                owner,
478                name,
479                product,
480                custom_domain: String::new(),
481            }
482        }
483    }
484}
485
486// organizations
487pub mod organizations {
488    use serde::{Deserialize, Serialize};
489    use std::collections::HashMap;
490    use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
491
492    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
493    pub enum OrganizationRole {
494        Owner,
495        Custom(usize),
496        #[default]
497        Member,
498    }
499
500    #[derive(Debug, Clone, Serialize, Deserialize)]
501    pub struct Organization {
502        pub id: usize,
503        pub created: usize,
504        pub owner: usize,
505        pub name: String,
506        pub members: i32,
507        pub roles: HashMap<usize, String>,
508    }
509
510    impl Organization {
511        /// Create a new [`Organization`].
512        pub fn new(name: String, owner: usize) -> Self {
513            Self {
514                id: Snowflake::new().to_string().parse::<usize>().unwrap(),
515                created: unix_epoch_timestamp(),
516                owner,
517                name,
518                members: 0,
519                roles: HashMap::new(),
520            }
521        }
522    }
523
524    #[derive(Debug, Clone, Serialize, Deserialize)]
525    pub struct OrganizationMembership {
526        pub id: usize,
527        pub created: usize,
528        pub organization: usize,
529        pub owner: usize,
530        pub role: OrganizationRole,
531    }
532
533    impl OrganizationMembership {
534        /// Create a new [`OrganizationMembership`].
535        pub fn new(organization: usize, owner: usize) -> Self {
536            Self {
537                id: Snowflake::new().to_string().parse::<usize>().unwrap(),
538                created: unix_epoch_timestamp(),
539                organization,
540                owner,
541                role: OrganizationRole::Member,
542            }
543        }
544    }
545
546    #[derive(Debug, Clone, Serialize, Deserialize)]
547    pub struct PermissionsList {
548        pub id: usize,
549        pub created: usize,
550        pub owner_org: usize,
551        pub name: String,
552        pub roles: Vec<usize>,
553    }
554
555    impl PermissionsList {
556        /// Create a new [`PermissionsList`].
557        pub fn new(owner_org: usize, name: String) -> Self {
558            Self {
559                id: Snowflake::new().to_string().parse::<usize>().unwrap(),
560                created: unix_epoch_timestamp(),
561                owner_org,
562                name,
563                roles: Vec::new(),
564            }
565        }
566    }
567}