torrust_index_backend/services/
user.rs

1//! User services.
2use std::sync::Arc;
3
4use argon2::password_hash::SaltString;
5use argon2::{Argon2, PasswordHasher};
6use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
7use log::{debug, info};
8use pbkdf2::password_hash::rand_core::OsRng;
9
10use crate::config::{Configuration, EmailOnSignup};
11use crate::databases::database::{Database, Error};
12use crate::errors::ServiceError;
13use crate::mailer;
14use crate::mailer::VerifyClaims;
15use crate::models::user::{UserCompact, UserId, UserProfile};
16use crate::utils::validation::validate_email_address;
17use crate::web::api::v1::contexts::user::forms::RegistrationForm;
18
19/// Since user email could be optional, we need a way to represent "no email"
20/// in the database. This function returns the string that should be used for
21/// that purpose.
22fn no_email() -> String {
23    String::new()
24}
25
26pub struct RegistrationService {
27    configuration: Arc<Configuration>,
28    mailer: Arc<mailer::Service>,
29    user_repository: Arc<DbUserRepository>,
30    user_profile_repository: Arc<DbUserProfileRepository>,
31}
32
33impl RegistrationService {
34    #[must_use]
35    pub fn new(
36        configuration: Arc<Configuration>,
37        mailer: Arc<mailer::Service>,
38        user_repository: Arc<DbUserRepository>,
39        user_profile_repository: Arc<DbUserProfileRepository>,
40    ) -> Self {
41        Self {
42            configuration,
43            mailer,
44            user_repository,
45            user_profile_repository,
46        }
47    }
48
49    /// It registers a new user.
50    ///
51    /// # Errors
52    ///
53    /// This function will return a:
54    ///
55    /// * `ServiceError::EmailMissing` if email is required, but missing.
56    /// * `ServiceError::EmailInvalid` if supplied email is badly formatted.
57    /// * `ServiceError::PasswordsDontMatch` if the supplied passwords do not match.
58    /// * `ServiceError::PasswordTooShort` if the supplied password is too short.
59    /// * `ServiceError::PasswordTooLong` if the supplied password is too long.
60    /// * `ServiceError::UsernameInvalid` if the supplied username is badly formatted.
61    /// * `ServiceError::FailedToSendVerificationEmail` if unable to send the required verification email.
62    /// * An error if unable to successfully hash the password.
63    /// * An error if unable to insert user into the database.
64    ///
65    /// # Panics
66    ///
67    /// This function will panic if the email is required, but missing.
68    pub async fn register_user(&self, registration_form: &RegistrationForm, api_base_url: &str) -> Result<UserId, ServiceError> {
69        info!("registering user: {}", registration_form.username);
70
71        let settings = self.configuration.settings.read().await;
72
73        let opt_email = match settings.auth.email_on_signup {
74            EmailOnSignup::Required => {
75                if registration_form.email.is_none() {
76                    return Err(ServiceError::EmailMissing);
77                }
78                registration_form.email.clone()
79            }
80            EmailOnSignup::None => None,
81            EmailOnSignup::Optional => registration_form.email.clone(),
82        };
83
84        if let Some(email) = &registration_form.email {
85            if !validate_email_address(email) {
86                return Err(ServiceError::EmailInvalid);
87            }
88        }
89
90        if registration_form.password != registration_form.confirm_password {
91            return Err(ServiceError::PasswordsDontMatch);
92        }
93
94        let password_length = registration_form.password.len();
95
96        if password_length <= settings.auth.min_password_length {
97            return Err(ServiceError::PasswordTooShort);
98        }
99
100        if password_length >= settings.auth.max_password_length {
101            return Err(ServiceError::PasswordTooLong);
102        }
103
104        let salt = SaltString::generate(&mut OsRng);
105
106        // Argon2 with default params (Argon2id v19)
107        let argon2 = Argon2::default();
108
109        // Hash password to PHC string ($argon2id$v=19$...)
110        let password_hash = argon2
111            .hash_password(registration_form.password.as_bytes(), &salt)?
112            .to_string();
113
114        if registration_form.username.contains('@') {
115            return Err(ServiceError::UsernameInvalid);
116        }
117
118        let user_id = self
119            .user_repository
120            .add(
121                &registration_form.username,
122                &opt_email.clone().unwrap_or(no_email()),
123                &password_hash,
124            )
125            .await?;
126
127        // If this is the first created account, give administrator rights
128        if user_id == 1 {
129            let _ = self.user_repository.grant_admin_role(&user_id).await;
130        }
131
132        if settings.mail.email_verification_enabled && opt_email.is_some() {
133            let mail_res = self
134                .mailer
135                .send_verification_mail(
136                    &opt_email.expect("variable `email` is checked above"),
137                    &registration_form.username,
138                    user_id,
139                    api_base_url,
140                )
141                .await;
142
143            if mail_res.is_err() {
144                let _ = self.user_repository.delete(&user_id).await;
145                return Err(ServiceError::FailedToSendVerificationEmail);
146            }
147        }
148
149        Ok(user_id)
150    }
151
152    /// It verifies the email address of a user via the token sent to the
153    /// user's email.
154    ///
155    /// # Errors
156    ///
157    /// This function will return a `ServiceError::DatabaseError` if unable to
158    /// update the user's email verification status.
159    pub async fn verify_email(&self, token: &str) -> Result<bool, ServiceError> {
160        let settings = self.configuration.settings.read().await;
161
162        let token_data = match decode::<VerifyClaims>(
163            token,
164            &DecodingKey::from_secret(settings.auth.secret_key.as_bytes()),
165            &Validation::new(Algorithm::HS256),
166        ) {
167            Ok(token_data) => {
168                if !token_data.claims.iss.eq("email-verification") {
169                    return Ok(false);
170                }
171
172                token_data.claims
173            }
174            Err(_) => return Ok(false),
175        };
176
177        drop(settings);
178
179        let user_id = token_data.sub;
180
181        if self.user_profile_repository.verify_email(&user_id).await.is_err() {
182            return Err(ServiceError::DatabaseError);
183        };
184
185        Ok(true)
186    }
187}
188
189pub struct BanService {
190    user_repository: Arc<DbUserRepository>,
191    user_profile_repository: Arc<DbUserProfileRepository>,
192    banned_user_list: Arc<DbBannedUserList>,
193}
194
195impl BanService {
196    #[must_use]
197    pub fn new(
198        user_repository: Arc<DbUserRepository>,
199        user_profile_repository: Arc<DbUserProfileRepository>,
200        banned_user_list: Arc<DbBannedUserList>,
201    ) -> Self {
202        Self {
203            user_repository,
204            user_profile_repository,
205            banned_user_list,
206        }
207    }
208
209    /// Ban a user from the Index.
210    ///
211    /// # Errors
212    ///
213    /// This function will return a:
214    ///
215    /// * `ServiceError::InternalServerError` if unable get user from the request.
216    /// * An error if unable to get user profile from supplied username.
217    /// * An error if unable to set the ban of the user in the database.
218    pub async fn ban_user(&self, username_to_be_banned: &str, user_id: &UserId) -> Result<(), ServiceError> {
219        debug!("user with ID {user_id} banning username: {username_to_be_banned}");
220
221        let user = self.user_repository.get_compact(user_id).await?;
222
223        // Check if user is administrator
224        if !user.administrator {
225            return Err(ServiceError::Unauthorized);
226        }
227
228        let user_profile = self
229            .user_profile_repository
230            .get_user_profile_from_username(username_to_be_banned)
231            .await?;
232
233        self.banned_user_list.add(&user_profile.user_id).await?;
234
235        Ok(())
236    }
237}
238
239pub struct DbUserRepository {
240    database: Arc<Box<dyn Database>>,
241}
242
243impl DbUserRepository {
244    #[must_use]
245    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
246        Self { database }
247    }
248
249    /// It returns the compact user.
250    ///
251    /// # Errors
252    ///
253    /// It returns an error if there is a database error.
254    pub async fn get_compact(&self, user_id: &UserId) -> Result<UserCompact, ServiceError> {
255        // todo: persistence layer should have its own errors instead of
256        // returning a `ServiceError`.
257        self.database
258            .get_user_compact_from_id(*user_id)
259            .await
260            .map_err(|_| ServiceError::UserNotFound)
261    }
262
263    /// It grants the admin role to the user.
264    ///
265    /// # Errors
266    ///
267    /// It returns an error if there is a database error.
268    pub async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error> {
269        self.database.grant_admin_role(*user_id).await
270    }
271
272    /// It deletes the user.
273    ///
274    /// # Errors
275    ///
276    /// It returns an error if there is a database error.
277    pub async fn delete(&self, user_id: &UserId) -> Result<(), Error> {
278        self.database.delete_user(*user_id).await
279    }
280
281    /// It adds a new user.
282    ///
283    /// # Errors
284    ///
285    /// It returns an error if there is a database error.
286    pub async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result<UserId, Error> {
287        self.database.insert_user_and_get_id(username, email, password_hash).await
288    }
289}
290
291pub struct DbUserProfileRepository {
292    database: Arc<Box<dyn Database>>,
293}
294
295impl DbUserProfileRepository {
296    #[must_use]
297    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
298        Self { database }
299    }
300
301    /// It marks the user's email as verified.
302    ///
303    /// # Errors
304    ///
305    /// It returns an error if there is a database error.
306    pub async fn verify_email(&self, user_id: &UserId) -> Result<(), Error> {
307        self.database.verify_email(*user_id).await
308    }
309
310    /// It get the user profile from the username.
311    ///
312    /// # Errors
313    ///
314    /// It returns an error if there is a database error.
315    pub async fn get_user_profile_from_username(&self, username: &str) -> Result<UserProfile, Error> {
316        self.database.get_user_profile_from_username(username).await
317    }
318}
319
320pub struct DbBannedUserList {
321    database: Arc<Box<dyn Database>>,
322}
323
324impl DbBannedUserList {
325    #[must_use]
326    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
327        Self { database }
328    }
329
330    /// It add a user to the banned users list.
331    ///
332    /// # Errors
333    ///
334    /// It returns an error if there is a database error.
335    ///
336    /// # Panics
337    ///
338    /// It panics if the expiration date cannot be parsed. It should never
339    /// happen as the date is hardcoded for now.
340    pub async fn add(&self, user_id: &UserId) -> Result<(), Error> {
341        // todo: add reason and `date_expiry` parameters to request.
342
343        // code-review: add the user ID of the user who banned the user.
344
345        // For the time being, we will not use a reason for banning a user.
346        let reason = "no reason".to_string();
347
348        // User will be banned until the year 9999
349        let date_expiry = chrono::NaiveDateTime::parse_from_str("9999-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
350            .expect("Could not parse date from 9999-01-01 00:00:00.");
351
352        self.database.ban_user(*user_id, &reason, date_expiry).await
353    }
354}