torrust_index/models/
user.rs

1use std::fmt;
2use std::str::FromStr;
3
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6
7#[allow(clippy::module_name_repetitions)]
8pub type UserId = i64;
9
10#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
11pub struct User {
12    pub user_id: UserId,
13    pub date_registered: Option<String>,
14    pub date_imported: Option<String>,
15    pub administrator: bool,
16}
17
18#[allow(clippy::module_name_repetitions)]
19#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
20pub struct UserAuthentication {
21    pub user_id: UserId,
22    pub password_hash: String,
23}
24
25#[allow(clippy::module_name_repetitions)]
26#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, sqlx::FromRow)]
27pub struct UserProfile {
28    pub user_id: UserId,
29    pub username: String,
30    pub email: String,
31    pub email_verified: bool,
32    pub bio: String,
33    pub avatar: String,
34}
35
36#[allow(clippy::module_name_repetitions)]
37#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
38pub struct UserCompact {
39    pub user_id: UserId,
40    pub username: String,
41    pub administrator: bool,
42}
43
44#[allow(clippy::module_name_repetitions)]
45#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
46pub struct UserFull {
47    pub user_id: UserId,
48    pub date_registered: Option<String>,
49    pub date_imported: Option<String>,
50    pub administrator: bool,
51    pub username: String,
52    pub email: String,
53    pub email_verified: bool,
54    pub bio: String,
55    pub avatar: String,
56}
57
58#[allow(clippy::module_name_repetitions)]
59#[derive(Debug, Serialize, Deserialize, Clone)]
60pub struct UserClaims {
61    pub user: UserCompact,
62    pub exp: u64, // epoch in seconds
63}
64
65const MAX_USERNAME_LENGTH: usize = 20;
66const USERNAME_VALIDATION_ERROR_MSG: &str = "Usernames must consist of 1-20 alphanumeric characters, dashes, or underscore";
67
68#[derive(Debug, Clone)]
69pub struct UsernameParseError {
70    message: String,
71}
72
73// Implement std::fmt::Display for UsernameParseError
74impl fmt::Display for UsernameParseError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "UsernameParseError: {}", self.message)
77    }
78}
79
80// Implement std::error::Error for UsernameParseError
81impl std::error::Error for UsernameParseError {}
82
83pub struct Username(String);
84
85impl fmt::Display for Username {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91// Implement the parsing logic
92impl FromStr for Username {
93    type Err = UsernameParseError;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        if s.len() > MAX_USERNAME_LENGTH {
97            return Err(UsernameParseError {
98                message: format!("username '{s}' is too long. {USERNAME_VALIDATION_ERROR_MSG}."),
99            });
100        }
101
102        let pattern = format!(r"^[A-Za-z0-9-_]{{1,{MAX_USERNAME_LENGTH}}}$");
103        let re = Regex::new(&pattern).expect("username regexp should be valid");
104
105        if re.is_match(s) {
106            Ok(Username(s.to_string()))
107        } else {
108            Err(UsernameParseError {
109                message: format!("'{s}' is not a valid username. {USERNAME_VALIDATION_ERROR_MSG}."),
110            })
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn username_must_consist_of_1_to_20_alphanumeric_characters_or_dashes() {
121        let username_str = "validUsername123";
122        assert!(username_str.parse::<Username>().is_ok());
123    }
124
125    #[test]
126    fn username_should_be_shorter_then_21_chars() {
127        let username_str = "a".repeat(MAX_USERNAME_LENGTH + 1);
128        assert!(username_str.parse::<Username>().is_err());
129    }
130
131    #[test]
132    fn username_should_not_allow_invalid_characters() {
133        let username_str = "invalid*Username";
134        assert!(username_str.parse::<Username>().is_err());
135    }
136
137    #[test]
138    fn username_should_be_displayed() {
139        let username = Username("FirstLast-01".to_string());
140        assert_eq!(username.to_string(), "FirstLast-01");
141    }
142}