torrust_index/models/
user.rs1use 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, }
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
73impl fmt::Display for UsernameParseError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "UsernameParseError: {}", self.message)
77 }
78}
79
80impl 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
91impl 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}