Skip to main content

tetratto_core2/database/
auth.rs

1use super::common::NAME_REGEX;
2use oiseau::cache::Cache;
3use crate::model::{
4    Error, Result,
5    auth::{
6        ACHIEVEMENTS, Achievement, AchievementName, AchievementRarity, Notification, Token, User,
7        UserConnections, UserSettings,
8    },
9    id::Id,
10    moderation::AuditLogEntry,
11    oauth::AuthGrant,
12    permissions::{FinePermission, PermissionsContainer},
13};
14use tritools::{
15    time::unix_epoch_timestamp,
16    encoding::{hash_salted, salt},
17};
18use crate::{auto_method, DataManager};
19use oiseau::{PostgresRow, execute, get, query_row, params};
20
21impl DataManager {
22    /// Get a [`User`] from an SQL row.
23    pub(crate) fn get_user_from_row(x: &PostgresRow) -> User {
24        let mut u = User {
25            id: crate::model::id::Id::deserialize(&get!(x->0(String))),
26            created: get!(x->1(i64)) as u128,
27            username: get!(x->2(String)),
28            password: get!(x->3(String)),
29            salt: get!(x->4(String)),
30            settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(),
31            tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
32            legacy_permissions: get!(x->7(i32)),
33            is_verified: get!(x->8(i32)) as i8 == 1,
34            notification_count: get!(x->9(i32)) as usize,
35            follower_count: get!(x->10(i32)) as usize,
36            following_count: get!(x->11(i32)) as usize,
37            last_seen: get!(x->12(i64)) as u128,
38            totp: get!(x->13(String)),
39            recovery_codes: serde_json::from_str(&get!(x->14(String)).to_string()).unwrap(),
40            post_count: get!(x->15(i32)) as usize,
41            request_count: get!(x->16(i32)) as usize,
42            connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(),
43            stripe_id: get!(x->18(String)),
44            grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(),
45            associated: serde_json::from_str(&get!(x->20(String)).to_string()).unwrap(),
46            achievements: serde_json::from_str(&get!(x->21(String)).to_string()).unwrap(),
47            ban_reason: get!(x->22(String)),
48            is_deactivated: get!(x->23(i32)) as i8 == 1,
49            ban_expire: get!(x->24(i64)) as u128,
50            checkouts: serde_json::from_str(&get!(x->25(String)).to_string()).unwrap(),
51            last_policy_consent: get!(x->26(i64)) as u128,
52            missed_messages_count: get!(x->27(i32)) as usize,
53            views: get!(x->28(i32)) as usize,
54            shrimpcamp_link: get!(x->29(i64)) as usize,
55            permissions: serde_json::from_str(&get!(x->30(String))).unwrap(),
56        };
57
58        // transfer the two legacy permissions we care about
59        if (u.legacy_permissions & 1 << 17) == 1 << 17 {
60            // banned
61            u.permissions.join(FinePermission::Banned);
62        }
63
64        if ((u.legacy_permissions & 1 << 19) == 1 << 19)
65            || ((u.legacy_permissions & 1 << 1) == 1 << 1)
66        {
67            // supporter
68            u.permissions.join(FinePermission::Supporter);
69        }
70
71        // ...
72        u
73    }
74
75    auto_method!(get_user_by_id()@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
76    auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
77    auto_method!(get_user_by_username_no_cache(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User);
78    auto_method!(get_user_by_browser_session(&str)@get_user_from_row -> "SELECT * FROM users WHERE browser_session = $1" --name="user" --returns=User);
79
80    /// Get a user given just their ID. Returns the void user if the user doesn't exist.
81    ///
82    /// # Arguments
83    /// * `id` - the ID of the user
84    pub async fn get_user_by_id_with_void(&self, id: &crate::model::id::Id) -> Result<User> {
85        let conn = match self.0.connect().await {
86            Ok(c) => c,
87            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
88        };
89
90        let res = query_row!(
91            &conn,
92            "SELECT * FROM users WHERE id = $1",
93            &[&id.printable()],
94            |x| Ok(Self::get_user_from_row(x))
95        );
96
97        if res.is_err() {
98            return Ok(User::deleted());
99            // return Err(Error::UserNotFound);
100        }
101
102        Ok(res.unwrap())
103    }
104
105    /// Get a user given just their auth token.
106    ///
107    /// # Arguments
108    /// * `token` - the token of the user
109    pub async fn get_user_by_token(&self, token: &str) -> Result<User> {
110        let conn = match self.0.connect().await {
111            Ok(c) => c,
112            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
113        };
114
115        let res = query_row!(
116            &conn,
117            "SELECT * FROM users WHERE tokens LIKE $1",
118            &[&format!("%\"{token}\"%")],
119            |x| Ok(Self::get_user_from_row(x))
120        );
121
122        if res.is_err() {
123            return Err(Error::UserNotFound);
124        }
125
126        let u = res.unwrap();
127
128        if u.legacy_permissions != 0
129            && (u.permissions.check(FinePermission::Banned)
130                || u.permissions.check(FinePermission::Supporter))
131        {
132            // clear legacy permissions (we've already moved over the user's permissions)
133            self.update_user_legacy_permissions(&u.id, 0).await?;
134            self.update_user_permissions(&u.id, u.permissions.clone())
135                .await?;
136        }
137
138        Ok(u)
139    }
140
141    /// Get a user given just their grant token.
142    ///
143    /// Also returns the auth grant this token is associated with from the user.
144    ///
145    /// # Arguments
146    /// * `token` - the token of the user
147    pub async fn get_user_by_grant_token(
148        &self,
149        token: &Id,
150        check_expiration: bool,
151    ) -> Result<(AuthGrant, User)> {
152        let conn = match self.0.connect().await {
153            Ok(c) => c,
154            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
155        };
156
157        let res = query_row!(
158            &conn,
159            "SELECT * FROM users WHERE grants LIKE $1",
160            &[&format!("%\"token\":\"{token}\"%")],
161            |x| Ok(Self::get_user_from_row(x))
162        );
163
164        if res.is_err() {
165            return Err(Error::UserNotFound);
166        }
167
168        let user = res.unwrap();
169        let grant = user
170            .grants
171            .iter()
172            .find(|x| x.token == *token)
173            .unwrap()
174            .clone();
175
176        // check token expiry
177        if check_expiration {
178            let now = unix_epoch_timestamp();
179            let delta = now - grant.last_updated;
180
181            if delta > 604_800_000 {
182                return Err(Error::MiscError("Token expired".to_string()));
183            }
184        }
185
186        // ...
187        Ok((grant, user))
188    }
189
190    /// Create a new user in the database.
191    ///
192    /// # Arguments
193    /// * `data` - a mock [`User`] object to insert
194    pub async fn create_user(&self, mut data: User) -> Result<()> {
195        if !self.0.0.security.registration_enabled {
196            return Err(Error::RegistrationDisabled);
197        }
198
199        data.username = data.username.to_lowercase();
200
201        // check values
202        if data.username.len() < 2 {
203            return Err(Error::DataTooShort("username".to_string()));
204        } else if data.username.len() > 32 {
205            return Err(Error::DataTooLong("username".to_string()));
206        }
207
208        if data.password.len() < 6 {
209            return Err(Error::DataTooShort("password".to_string()));
210        }
211
212        if self.0.0.banned_usernames.contains(&data.username) {
213            return Err(Error::MiscError("This username cannot be used".to_string()));
214        }
215
216        let regex = regex::RegexBuilder::new(NAME_REGEX)
217            .multi_line(true)
218            .build()
219            .unwrap();
220
221        if regex.captures(&data.username).is_some() {
222            return Err(Error::MiscError(
223                "This username contains invalid characters".to_string(),
224            ));
225        }
226
227        // make sure username isn't taken
228        if self.get_user_by_username(&data.username).await.is_ok() {
229            return Err(Error::UsernameInUse);
230        }
231
232        // ...
233        let conn = match self.0.connect().await {
234            Ok(c) => c,
235            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
236        };
237
238        let res = execute!(
239            &conn,
240            "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)",
241            params![
242                &data.id.printable(),
243                &(data.created as i64),
244                &data.username.to_lowercase(),
245                &data.password,
246                &data.salt,
247                &serde_json::to_string(&data.settings).unwrap(),
248                &serde_json::to_string(&data.tokens).unwrap(),
249                &0_i32,
250                &if data.is_verified { 1_i32 } else { 0_i32 },
251                &0_i32,
252                &0_i32,
253                &0_i32,
254                &(data.last_seen as i64),
255                &String::new(),
256                "[]",
257                &0_i32,
258                &0_i32,
259                &serde_json::to_string(&data.connections).unwrap(),
260                &"",
261                &serde_json::to_string(&data.grants).unwrap(),
262                &serde_json::to_string(&data.associated).unwrap(),
263                &serde_json::to_string(&data.achievements).unwrap(),
264                &data.ban_reason,
265                &if data.is_deactivated { 1_i32 } else { 0_i32 },
266                &(data.ban_expire as i64),
267                &serde_json::to_string(&data.checkouts).unwrap(),
268                &(data.last_policy_consent as i64),
269                &(data.missed_messages_count as i32),
270                &(data.views as i32),
271                &(data.shrimpcamp_link as i64),
272                &serde_json::to_string(&data.permissions).unwrap()
273            ]
274        );
275
276        if let Err(e) = res {
277            return Err(Error::DatabaseError(e.to_string()));
278        }
279
280        Ok(())
281    }
282
283    /// Delete an existing user in the database.
284    ///
285    /// # Arguments
286    /// * `id` - the ID of the user
287    /// * `password` - the current password of the user
288    /// * `force` - if we should delete even if the given password is incorrect
289    pub async fn delete_user(&self, id: &Id, password: &str, force: bool) -> Result<User> {
290        let user = self.get_user_by_id(id).await?;
291
292        if (hash_salted(password.to_string(), user.salt.clone()) != user.password) && !force {
293            return Err(Error::IncorrectPassword);
294        }
295
296        if user.permissions.check(FinePermission::Supporter) {
297            return Err(Error::MiscError(
298                "Please cancel your supporter membership before attempting to delete your account"
299                    .to_string(),
300            ));
301        }
302
303        let conn = match self.0.connect().await {
304            Ok(c) => c,
305            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
306        };
307
308        let res = execute!(&conn, "DELETE FROM users WHERE id = $1", &[&id.printable()]);
309
310        if let Err(e) = res {
311            return Err(Error::DatabaseError(e.to_string()));
312        }
313
314        self.cache_clear_user(&user).await;
315
316        // delete uploads
317        for upload in match self.1.get_uploads_by_owner_all(user.id.as_usize()).await {
318            Ok(x) => x,
319            Err(e) => return Err(Error::MiscError(e.to_string())),
320        } {
321            if let Err(e) = self.1.delete_upload(upload.id).await {
322                return Err(Error::MiscError(e.to_string()));
323            }
324        }
325
326        // ...
327        Ok(user)
328    }
329
330    pub async fn update_user_verified_status(&self, id: &Id, x: bool, user: User) -> Result<()> {
331        if !user.permissions.check(FinePermission::ManageVerified) {
332            return Err(Error::NotAllowed);
333        }
334
335        let other_user = self.get_user_by_id(id).await?;
336
337        let conn = match self.0.connect().await {
338            Ok(c) => c,
339            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
340        };
341
342        let res = execute!(
343            &conn,
344            "UPDATE users SET verified = $1 WHERE id = $2",
345            params![&{ if x { 1 } else { 0 } }, &id.printable()]
346        );
347
348        if let Err(e) = res {
349            return Err(Error::DatabaseError(e.to_string()));
350        }
351
352        self.cache_clear_user(&other_user).await;
353
354        // create audit log entry
355        self.create_audit_log_entry(AuditLogEntry::new(
356            user.id,
357            format!(
358                "invoked `update_user_verified_status` with x value `{}` and y value `{}`",
359                other_user.id, x
360            ),
361        ))
362        .await?;
363
364        // ...
365        Ok(())
366    }
367
368    pub async fn update_user_is_deactivated(
369        &self,
370        id: &crate::model::id::Id,
371        x: bool,
372        user: User,
373    ) -> Result<()> {
374        if *id != user.id && !user.permissions.check(FinePermission::ManageUsers) {
375            return Err(Error::NotAllowed);
376        }
377
378        let other_user = self.get_user_by_id(id).await?;
379
380        let conn = match self.0.connect().await {
381            Ok(c) => c,
382            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
383        };
384
385        let res = execute!(
386            &conn,
387            "UPDATE users SET is_deactivated = $1 WHERE id = $2",
388            params![&{ if x { 1 } else { 0 } }, &id.printable()]
389        );
390
391        if let Err(e) = res {
392            return Err(Error::DatabaseError(e.to_string()));
393        }
394
395        self.cache_clear_user(&other_user).await;
396
397        // create audit log entry (if we aren't the user that is being updated)
398        if user.id != other_user.id {
399            self.create_audit_log_entry(AuditLogEntry::new(
400                user.id,
401                format!(
402                    "invoked `update_user_is_deactivated` with x value `{}` and y value `{}`",
403                    other_user.id, x
404                ),
405            ))
406            .await?;
407        }
408
409        // ...
410        Ok(())
411    }
412
413    pub async fn update_user_password(
414        &self,
415        id: &crate::model::id::Id,
416        from: String,
417        to: String,
418        user: User,
419        force: bool,
420    ) -> Result<()> {
421        // verify password
422        if !user.check_password(from.clone()) && !force {
423            return Err(Error::MiscError("Password does not match".to_string()));
424        }
425
426        // ...
427        let conn = match self.0.connect().await {
428            Ok(c) => c,
429            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
430        };
431
432        let new_salt = salt();
433        let new_password = hash_salted(to, new_salt.clone());
434        let res = execute!(
435            &conn,
436            "UPDATE users SET password = $1, salt = $2 WHERE id = $3",
437            params![&new_password.as_str(), &new_salt.as_str(), &id.printable()]
438        );
439
440        if let Err(e) = res {
441            return Err(Error::DatabaseError(e.to_string()));
442        }
443
444        self.cache_clear_user(&user).await;
445        Ok(())
446    }
447
448    pub async fn update_user_username(
449        &self,
450        id: &crate::model::id::Id,
451        to: String,
452        user: User,
453    ) -> Result<()> {
454        // check value
455        if to.len() < 2 {
456            return Err(Error::DataTooShort("username".to_string()));
457        } else if to.len() > 32 {
458            return Err(Error::DataTooLong("username".to_string()));
459        }
460
461        if self.0.0.banned_usernames.contains(&to) {
462            return Err(Error::MiscError("This username cannot be used".to_string()));
463        }
464
465        let regex = regex::RegexBuilder::new(r"[^\w_\-\.!]+")
466            .multi_line(true)
467            .build()
468            .unwrap();
469
470        if regex.captures(&to).is_some() {
471            return Err(Error::MiscError(
472                "This username contains invalid characters".to_string(),
473            ));
474        }
475
476        // ...
477        let conn = match self.0.connect().await {
478            Ok(c) => c,
479            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
480        };
481
482        let res = execute!(
483            &conn,
484            "UPDATE users SET username = $1 WHERE id = $2",
485            params![&to.to_lowercase(), &id.printable()]
486        );
487
488        if let Err(e) = res {
489            return Err(Error::DatabaseError(e.to_string()));
490        }
491
492        self.cache_clear_user(&user).await;
493        Ok(())
494    }
495
496    pub async fn update_user_awaiting_purchased_status(
497        &self,
498        id: &crate::model::id::Id,
499        x: bool,
500        user: User,
501        require_permission: bool,
502    ) -> Result<()> {
503        if (user.id != *id) | require_permission
504            && !user.permissions.check(FinePermission::ManageUsers)
505        {
506            return Err(Error::NotAllowed);
507        }
508
509        let other_user = self.get_user_by_id(id).await?;
510
511        let conn = match self.0.connect().await {
512            Ok(c) => c,
513            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
514        };
515
516        let res = execute!(
517            &conn,
518            "UPDATE users SET awaiting_purchase = $1 WHERE id = $2",
519            params![&{ if x { 1 } else { 0 } }, &id.printable()]
520        );
521
522        if let Err(e) = res {
523            return Err(Error::DatabaseError(e.to_string()));
524        }
525
526        self.cache_clear_user(&other_user).await;
527
528        // create audit log entry
529        if user.id != other_user.id {
530            self.create_audit_log_entry(AuditLogEntry::new(
531                user.id,
532                format!(
533                    "invoked `update_user_purchased_status` with x value `{}` and y value `{}`",
534                    other_user.id, x
535                ),
536            ))
537            .await?;
538        }
539
540        // ...
541        Ok(())
542    }
543
544    pub async fn seen_user(&self, user: &User) -> Result<()> {
545        let conn = match self.0.connect().await {
546            Ok(c) => c,
547            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
548        };
549
550        let res = execute!(
551            &conn,
552            "UPDATE users SET last_seen = $1 WHERE id = $2",
553            params![&(unix_epoch_timestamp() as i64), &user.id.printable()]
554        );
555
556        if let Err(e) = res {
557            return Err(Error::DatabaseError(e.to_string()));
558        }
559
560        self.cache_clear_user(user).await;
561
562        Ok(())
563    }
564
565    /// Add an achievement to a user.
566    ///
567    /// Still returns `Ok` if the user already has the achievement.
568    #[async_recursion::async_recursion]
569    pub async fn add_achievement(
570        &self,
571        user: &mut User,
572        achievement: Achievement,
573        check_for_final: bool,
574    ) -> Result<()> {
575        if user.settings.disable_achievements {
576            return Ok(());
577        }
578
579        if user.achievements.iter().any(|x| x.name == achievement.name) {
580            return Ok(());
581        }
582
583        // send notif
584        self.create_notification(Notification::new(
585            "You've earned a new achievement!".to_string(),
586            format!(
587                "You've earned the \"{}\" [achievement](/user/achievements)!",
588                achievement.name.title()
589            ),
590            user.id.to_owned(),
591        ))
592        .await?;
593
594        // add achievement
595        user.achievements.push(achievement);
596        self.update_user_achievements(&user.id, user.achievements.to_owned())
597            .await?;
598
599        // check for final
600        if check_for_final && user.achievements.len() + 1 == ACHIEVEMENTS {
601            self.add_achievement(user, AchievementName::GetAllOtherAchievements.into(), false)
602                .await?;
603        }
604
605        // ...
606        Ok(())
607    }
608
609    /// Fill achievements with their title and description.
610    ///
611    /// # Returns
612    /// `(name, description, rarity, achievement)`
613    pub fn fill_achievements(
614        &self,
615        mut list: Vec<Achievement>,
616    ) -> Vec<(String, String, AchievementRarity, Achievement)> {
617        let mut out = Vec::new();
618
619        // sort by unlocked desc
620        list.sort_by(|a, b| a.unlocked.cmp(&b.unlocked));
621        list.reverse();
622
623        // ...
624        for x in list {
625            out.push((
626                x.name.title().to_string(),
627                x.name.description().to_string(),
628                x.name.rarity(),
629                x,
630            ))
631        }
632
633        out
634    }
635
636    /// Validate a given TOTP code for the given profile.
637    pub fn check_totp(&self, ua: &User, code: &str) -> bool {
638        let totp = ua.totp(Some(
639            self.0
640                .0
641                .host
642                .replace("http://", "")
643                .replace("https://", "")
644                .replace(":", "_"),
645        ));
646
647        if let Some(totp) = totp {
648            return !code.is_empty()
649                && (totp.check_current(code).unwrap()
650                    | ua.recovery_codes.contains(&code.to_string()));
651        }
652
653        true
654    }
655
656    /// Generate 8 random recovery codes for TOTP.
657    pub fn generate_totp_recovery_codes() -> Vec<String> {
658        let mut out: Vec<String> = Vec::new();
659
660        for _ in 0..9 {
661            out.push(salt())
662        }
663
664        out
665    }
666
667    /// Update the profile's TOTP secret.
668    ///
669    /// # Arguments
670    /// * `id` - the ID of the user
671    /// * `secret` - the TOTP secret
672    /// * `recovery` - the TOTP recovery codes
673    pub async fn update_user_totp(
674        &self,
675        id: &crate::model::id::Id,
676        secret: &str,
677        recovery: &Vec<String>,
678    ) -> Result<()> {
679        let user = self.get_user_by_id(id).await?;
680
681        // update
682        let conn = match self.0.connect().await {
683            Ok(c) => c,
684            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
685        };
686
687        let res = execute!(
688            &conn,
689            "UPDATE users SET totp = $1, recovery_codes = $2 WHERE id = $3",
690            params![
691                &secret,
692                &serde_json::to_string(recovery).unwrap(),
693                &id.printable()
694            ]
695        );
696
697        if let Err(e) = res {
698            return Err(Error::DatabaseError(e.to_string()));
699        }
700
701        self.cache_clear_user(&user).await;
702        Ok(())
703    }
704
705    /// Enable TOTP for a profile.
706    ///
707    /// # Arguments
708    /// * `id` - the ID of the user to enable TOTP for
709    /// * `user` - the user doing this
710    ///
711    /// # Returns
712    /// `Result<(secret, qr base64)>`
713    pub async fn enable_totp(
714        &self,
715        id: &crate::model::id::Id,
716        user: User,
717    ) -> Result<(String, String, Vec<String>)> {
718        let other_user = self.get_user_by_id(id).await?;
719
720        if other_user.id != user.id {
721            if other_user.permissions.check(FinePermission::ManageUsers) {
722                // create audit log entry
723                self.create_audit_log_entry(AuditLogEntry::new(
724                    user.id,
725                    format!("invoked `enable_totp` with x value `{}`", other_user.id,),
726                ))
727                .await?;
728            } else {
729                return Err(Error::NotAllowed);
730            }
731        }
732
733        let secret = totp_rs::Secret::default().to_string();
734        let recovery = Self::generate_totp_recovery_codes();
735        self.update_user_totp(id, &secret, &recovery).await?;
736
737        // fetch profile again (with totp information)
738        let other_user = self.get_user_by_id(id).await?;
739
740        // get totp
741        let totp = other_user.totp(Some(
742            self.0
743                .0
744                .host
745                .replace("http://", "")
746                .replace("https://", "")
747                .replace(":", "_"),
748        ));
749
750        if totp.is_none() {
751            return Err(Error::MiscError("Failed to get TOTP code".to_string()));
752        }
753
754        let totp = totp.unwrap();
755
756        // generate qr
757        let qr = match totp.get_qr_base64() {
758            Ok(q) => q,
759            Err(e) => return Err(Error::MiscError(e.to_string())),
760        };
761
762        // return
763        Ok((totp.get_secret_base32(), qr, recovery))
764    }
765
766    pub async fn cache_clear_user(&self, user: &User) {
767        self.0.1.remove(format!("atto.user:{}", user.id)).await;
768        self.0
769            .1
770            .remove(format!("atto.user:{}", user.username))
771            .await;
772    }
773
774    auto_method!(update_user_permissions(PermissionsContainer)@get_user_by_id -> "UPDATE users SET permissions = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
775    auto_method!(update_user_tokens(Vec<Token>)@get_user_by_id -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
776    auto_method!(update_user_grants(Vec<AuthGrant>)@get_user_by_id -> "UPDATE users SET grants = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
777    auto_method!(update_user_settings(UserSettings)@get_user_by_id -> "UPDATE users SET settings = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
778    auto_method!(update_user_connections(UserConnections)@get_user_by_id -> "UPDATE users SET connections = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
779    auto_method!(update_user_associated(Vec<Id>)@get_user_by_id -> "UPDATE users SET associated = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
780    auto_method!(update_user_achievements(Vec<Achievement>)@get_user_by_id -> "UPDATE users SET achievements = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
781    auto_method!(update_user_ban_reason(&str)@get_user_by_id -> "UPDATE users SET ban_reason = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
782    auto_method!(update_user_channel_mutes(Vec<usize>)@get_user_by_id -> "UPDATE users SET channel_mutes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
783    auto_method!(update_user_ban_expire(i64)@get_user_by_id -> "UPDATE users SET ban_expire = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
784    auto_method!(update_user_checkouts(Vec<String>)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
785    auto_method!(update_user_last_policy_consent(i64)@get_user_by_id -> "UPDATE users SET last_policy_consent = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
786    auto_method!(update_user_close_friends_stack(i64)@get_user_by_id -> "UPDATE users SET close_friends_stack = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
787    auto_method!(update_user_shrimpcamp_link(i64)@get_user_by_id -> "UPDATE users SET shrimpcamp_link = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
788    auto_method!(update_user_legacy_permissions(i32)@get_user_by_id -> "UPDATE users SET legacy_permissions = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
789
790    auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User);
791    auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
792
793    auto_method!(update_user_notification_count(i32)@get_user_by_id -> "UPDATE users SET notification_count = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
794    auto_method!(incr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
795    auto_method!(decr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=notification_count);
796
797    auto_method!(incr_user_follower_count()@get_user_by_id -> "UPDATE users SET follower_count = follower_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
798    auto_method!(decr_user_follower_count()@get_user_by_id -> "UPDATE users SET follower_count = follower_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=follower_count);
799
800    auto_method!(incr_user_following_count()@get_user_by_id -> "UPDATE users SET following_count = following_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
801    auto_method!(decr_user_following_count()@get_user_by_id -> "UPDATE users SET following_count = following_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=following_count);
802
803    auto_method!(incr_user_post_count()@get_user_by_id -> "UPDATE users SET post_count = post_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
804    auto_method!(decr_user_post_count()@get_user_by_id -> "UPDATE users SET post_count = post_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=post_count);
805
806    auto_method!(update_user_request_count(i32)@get_user_by_id -> "UPDATE users SET request_count = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
807    auto_method!(incr_user_request_count()@get_user_by_id -> "UPDATE users SET request_count = request_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
808    auto_method!(decr_user_request_count()@get_user_by_id -> "UPDATE users SET request_count = request_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=request_count);
809
810    auto_method!(update_user_missed_messages_count(i32)@get_user_by_id -> "UPDATE users SET missed_messages_count = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
811    auto_method!(incr_user_missed_messages()@get_user_by_id -> "UPDATE users SET missed_messages_count = missed_messages_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
812    auto_method!(decr_user_missed_messages()@get_user_by_id -> "UPDATE users SET missed_messages_count = missed_messages_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=notification_count);
813
814    auto_method!(incr_profile_views()@get_user_by_id -> "UPDATE users SET views = views + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
815    auto_method!(decr_profile_views()@get_user_by_id -> "UPDATE users SET views = views - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=views);
816}