torrust_index/services/
user.rs

1//! User services.
2use std::sync::Arc;
3
4use argon2::password_hash::SaltString;
5use argon2::{Argon2, PasswordHasher};
6use async_trait::async_trait;
7use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
8#[cfg(test)]
9use mockall::automock;
10use pbkdf2::password_hash::rand_core::OsRng;
11use tracing::{debug, info};
12
13use super::authentication::DbUserAuthenticationRepository;
14use super::authorization::{self, ACTION};
15use crate::config::{Configuration, PasswordConstraints};
16use crate::databases::database::{Database, Error};
17use crate::errors::ServiceError;
18use crate::mailer;
19use crate::mailer::VerifyClaims;
20use crate::models::user::{UserCompact, UserId, UserProfile, Username};
21use crate::services::authentication::verify_password;
22use crate::utils::validation::validate_email_address;
23use crate::web::api::server::v1::contexts::user::forms::{ChangePasswordForm, RegistrationForm};
24
25/// Since user email could be optional, we need a way to represent "no email"
26/// in the database. This function returns the string that should be used for
27/// that purpose.
28fn no_email() -> String {
29    String::new()
30}
31
32pub struct RegistrationService {
33    configuration: Arc<Configuration>,
34    mailer: Arc<mailer::Service>,
35    user_repository: Arc<Box<dyn Repository>>,
36    user_profile_repository: Arc<DbUserProfileRepository>,
37}
38
39impl RegistrationService {
40    #[must_use]
41    pub fn new(
42        configuration: Arc<Configuration>,
43        mailer: Arc<mailer::Service>,
44        user_repository: Arc<Box<dyn Repository>>,
45        user_profile_repository: Arc<DbUserProfileRepository>,
46    ) -> Self {
47        Self {
48            configuration,
49            mailer,
50            user_repository,
51            user_profile_repository,
52        }
53    }
54
55    /// It registers a new user.
56    ///
57    /// # Errors
58    ///
59    /// This function will return a:
60    ///
61    /// * `ServiceError::EmailMissing` if email is required, but missing.
62    /// * `ServiceError::EmailInvalid` if supplied email is badly formatted.
63    /// * `ServiceError::PasswordsDontMatch` if the supplied passwords do not match.
64    /// * `ServiceError::PasswordTooShort` if the supplied password is too short.
65    /// * `ServiceError::PasswordTooLong` if the supplied password is too long.
66    /// * `ServiceError::UsernameInvalid` if the supplied username is badly formatted.
67    /// * `ServiceError::FailedToSendVerificationEmail` if unable to send the required verification email.
68    /// * An error if unable to successfully hash the password.
69    /// * An error if unable to insert user into the database.
70    ///
71    /// # Panics
72    ///
73    /// This function will panic if the email is required, but missing.
74    pub async fn register_user(&self, registration_form: &RegistrationForm, api_base_url: &str) -> Result<UserId, ServiceError> {
75        info!("registering user: {}", registration_form.username);
76
77        let settings = self.configuration.settings.read().await;
78
79        match &settings.registration {
80            Some(registration) => {
81                let Ok(username) = registration_form.username.parse::<Username>() else {
82                    return Err(ServiceError::UsernameInvalid);
83                };
84
85                let opt_email = match &registration.email {
86                    Some(email) => {
87                        if email.required && registration_form.email.is_none() {
88                            return Err(ServiceError::EmailMissing);
89                        }
90                        match &registration_form.email {
91                            Some(email) => {
92                                if email.trim() == String::new() {
93                                    None
94                                } else {
95                                    Some(email.clone())
96                                }
97                            }
98                            None => None,
99                        }
100                    }
101                    None => None,
102                };
103
104                if let Some(email) = &opt_email {
105                    if !validate_email_address(email) {
106                        return Err(ServiceError::EmailInvalid);
107                    }
108                }
109
110                let password_constraints = PasswordConstraints {
111                    min_password_length: settings.auth.password_constraints.min_password_length,
112                    max_password_length: settings.auth.password_constraints.max_password_length,
113                };
114
115                validate_password_constraints(
116                    &registration_form.password,
117                    &registration_form.confirm_password,
118                    &password_constraints,
119                )?;
120
121                let password_hash = hash_password(&registration_form.password)?;
122
123                let user_id = self
124                    .user_repository
125                    .add(
126                        &username.to_string(),
127                        &opt_email.clone().unwrap_or(no_email()),
128                        &password_hash,
129                    )
130                    .await?;
131
132                // If this is the first created account, give administrator rights
133                if user_id == 1 {
134                    drop(self.user_repository.grant_admin_role(&user_id).await);
135                }
136
137                if let Some(email) = &registration.email {
138                    if email.verification_required {
139                        // Email verification is enabled
140                        if let Some(email) = opt_email {
141                            let mail_res = self
142                                .mailer
143                                .send_verification_mail(&email, &registration_form.username, user_id, api_base_url)
144                                .await;
145
146                            if mail_res.is_err() {
147                                drop(self.user_repository.delete(&user_id).await);
148                                return Err(ServiceError::FailedToSendVerificationEmail);
149                            }
150                        }
151                    }
152                }
153
154                Ok(user_id)
155            }
156            None => Err(ServiceError::ClosedForRegistration),
157        }
158    }
159
160    /// It verifies the email address of a user via the token sent to the
161    /// user's email.
162    ///
163    /// # Errors
164    ///
165    /// This function will return a `ServiceError::DatabaseError` if unable to
166    /// update the user's email verification status.
167    pub async fn verify_email(&self, token: &str) -> Result<bool, ServiceError> {
168        let settings = self.configuration.settings.read().await;
169
170        let token_data = match decode::<VerifyClaims>(
171            token,
172            &DecodingKey::from_secret(settings.auth.user_claim_token_pepper.as_bytes()),
173            &Validation::new(Algorithm::HS256),
174        ) {
175            Ok(token_data) => {
176                if !token_data.claims.iss.eq("email-verification") {
177                    return Ok(false);
178                }
179
180                token_data.claims
181            }
182            Err(_) => return Ok(false),
183        };
184
185        drop(settings);
186
187        let user_id = token_data.sub;
188
189        if self.user_profile_repository.verify_email(&user_id).await.is_err() {
190            return Err(ServiceError::DatabaseError);
191        };
192
193        Ok(true)
194    }
195}
196
197pub struct ProfileService {
198    configuration: Arc<Configuration>,
199    user_authentication_repository: Arc<DbUserAuthenticationRepository>,
200    authorization_service: Arc<authorization::Service>,
201}
202
203impl ProfileService {
204    #[must_use]
205    pub fn new(
206        configuration: Arc<Configuration>,
207        user_repository: Arc<DbUserAuthenticationRepository>,
208        authorization_service: Arc<authorization::Service>,
209    ) -> Self {
210        Self {
211            configuration,
212            user_authentication_repository: user_repository,
213            authorization_service,
214        }
215    }
216
217    /// It registers a new user.
218    ///
219    /// # Errors
220    ///
221    /// This function will return a:
222    ///
223    /// * `ServiceError::InvalidPassword` if the current password supplied is invalid.
224    /// * `ServiceError::PasswordsDontMatch` if the supplied passwords do not match.
225    /// * `ServiceError::PasswordTooShort` if the supplied password is too short.
226    /// * `ServiceError::PasswordTooLong` if the supplied password is too long.
227    /// * An error if unable to successfully hash the password.
228    /// * An error if unable to change the password in the database.
229    /// * An error if it is not possible to authorize the action
230    pub async fn change_password(
231        &self,
232        maybe_user_id: Option<UserId>,
233        change_password_form: &ChangePasswordForm,
234    ) -> Result<(), ServiceError> {
235        let Some(user_id) = maybe_user_id else {
236            return Err(ServiceError::UnauthorizedActionForGuests);
237        };
238
239        self.authorization_service
240            .authorize(ACTION::ChangePassword, maybe_user_id)
241            .await?;
242
243        info!("changing user password for user ID: {}", user_id);
244
245        let settings = self.configuration.settings.read().await;
246
247        let user_authentication = self
248            .user_authentication_repository
249            .get_user_authentication_from_id(&user_id)
250            .await?;
251
252        verify_password(change_password_form.current_password.as_bytes(), &user_authentication)?;
253
254        let password_constraints = PasswordConstraints {
255            min_password_length: settings.auth.password_constraints.min_password_length,
256            max_password_length: settings.auth.password_constraints.max_password_length,
257        };
258
259        validate_password_constraints(
260            &change_password_form.password,
261            &change_password_form.confirm_password,
262            &password_constraints,
263        )?;
264
265        let password_hash = hash_password(&change_password_form.password)?;
266
267        self.user_authentication_repository
268            .change_password(user_id, &password_hash)
269            .await?;
270
271        Ok(())
272    }
273}
274
275pub struct BanService {
276    user_profile_repository: Arc<DbUserProfileRepository>,
277    banned_user_list: Arc<DbBannedUserList>,
278    authorization_service: Arc<authorization::Service>,
279}
280
281impl BanService {
282    #[must_use]
283    pub fn new(
284        user_profile_repository: Arc<DbUserProfileRepository>,
285        banned_user_list: Arc<DbBannedUserList>,
286        authorization_service: Arc<authorization::Service>,
287    ) -> Self {
288        Self {
289            user_profile_repository,
290            banned_user_list,
291            authorization_service,
292        }
293    }
294
295    /// Ban a user from the Index.
296    ///
297    /// # Errors
298    ///
299    /// This function will return a:
300    ///
301    /// * `ServiceError::InternalServerError` if unable get user from the request.
302    /// * An error if unable to get user profile from supplied username.
303    /// * An error if unable to set the ban of the user in the database.
304    pub async fn ban_user(&self, username_to_be_banned: &str, maybe_user_id: Option<UserId>) -> Result<(), ServiceError> {
305        let Some(user_id) = maybe_user_id else {
306            return Err(ServiceError::UnauthorizedActionForGuests);
307        };
308
309        self.authorization_service.authorize(ACTION::BanUser, maybe_user_id).await?;
310
311        debug!("user with ID {} banning username: {username_to_be_banned}", user_id);
312
313        let user_profile = self
314            .user_profile_repository
315            .get_user_profile_from_username(username_to_be_banned)
316            .await?;
317
318        self.banned_user_list.add(&user_profile.user_id).await?;
319
320        Ok(())
321    }
322}
323
324#[cfg_attr(test, automock)]
325#[async_trait]
326pub trait Repository: Sync + Send {
327    async fn get_compact(&self, user_id: &UserId) -> Result<UserCompact, ServiceError>;
328    async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error>;
329    async fn delete(&self, user_id: &UserId) -> Result<(), Error>;
330    async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result<UserId, Error>;
331}
332
333pub struct DbUserRepository {
334    database: Arc<Box<dyn Database>>,
335}
336
337impl DbUserRepository {
338    #[must_use]
339    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
340        Self { database }
341    }
342}
343
344#[async_trait]
345impl Repository for DbUserRepository {
346    /// It returns the compact user.
347    ///
348    /// # Errors
349    ///
350    /// It returns an error if there is a database error.
351    async fn get_compact(&self, user_id: &UserId) -> Result<UserCompact, ServiceError> {
352        // todo: persistence layer should have its own errors instead of
353        // returning a `ServiceError`.
354        self.database
355            .get_user_compact_from_id(*user_id)
356            .await
357            .map_err(|_| ServiceError::UserNotFound)
358    }
359
360    /// It grants the admin role to the user.
361    ///
362    /// # Errors
363    ///
364    /// It returns an error if there is a database error.
365    async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error> {
366        self.database.grant_admin_role(*user_id).await
367    }
368
369    /// It deletes the user.
370    ///
371    /// # Errors
372    ///
373    /// It returns an error if there is a database error.
374    async fn delete(&self, user_id: &UserId) -> Result<(), Error> {
375        self.database.delete_user(*user_id).await
376    }
377
378    /// It adds a new user.
379    ///
380    /// # Errors
381    ///
382    /// It returns an error if there is a database error.
383    async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result<UserId, Error> {
384        self.database.insert_user_and_get_id(username, email, password_hash).await
385    }
386}
387
388pub struct DbUserProfileRepository {
389    database: Arc<Box<dyn Database>>,
390}
391
392impl DbUserProfileRepository {
393    #[must_use]
394    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
395        Self { database }
396    }
397
398    /// It marks the user's email as verified.
399    ///
400    /// # Errors
401    ///
402    /// It returns an error if there is a database error.
403    pub async fn verify_email(&self, user_id: &UserId) -> Result<(), Error> {
404        self.database.verify_email(*user_id).await
405    }
406
407    /// It get the user profile from the username.
408    ///
409    /// # Errors
410    ///
411    /// It returns an error if there is a database error.
412    pub async fn get_user_profile_from_username(&self, username: &str) -> Result<UserProfile, Error> {
413        self.database.get_user_profile_from_username(username).await
414    }
415}
416
417pub struct DbBannedUserList {
418    database: Arc<Box<dyn Database>>,
419}
420
421impl DbBannedUserList {
422    #[must_use]
423    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
424        Self { database }
425    }
426
427    /// It add a user to the banned users list.
428    ///
429    /// # Errors
430    ///
431    /// It returns an error if there is a database error.
432    ///
433    /// # Panics
434    ///
435    /// It panics if the expiration date cannot be parsed. It should never
436    /// happen as the date is hardcoded for now.
437    pub async fn add(&self, user_id: &UserId) -> Result<(), Error> {
438        // todo: add reason and `date_expiry` parameters to request.
439
440        // code-review: add the user ID of the user who banned the user.
441
442        // For the time being, we will not use a reason for banning a user.
443        let reason = "no reason".to_string();
444
445        // User will be banned until the year 9999
446        let date_expiry = chrono::NaiveDateTime::parse_from_str("9999-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
447            .expect("Could not parse date from 9999-01-01 00:00:00.");
448
449        self.database.ban_user(*user_id, &reason, date_expiry).await
450    }
451}
452
453fn validate_password_constraints(
454    password: &str,
455    confirm_password: &str,
456    password_rules: &PasswordConstraints,
457) -> Result<(), ServiceError> {
458    if password != confirm_password {
459        return Err(ServiceError::PasswordsDontMatch);
460    }
461
462    let password_length = password.len();
463
464    if password_length < password_rules.min_password_length {
465        return Err(ServiceError::PasswordTooShort);
466    }
467
468    if password_length > password_rules.max_password_length {
469        return Err(ServiceError::PasswordTooLong);
470    }
471
472    Ok(())
473}
474
475fn hash_password(password: &str) -> Result<String, ServiceError> {
476    let salt = SaltString::generate(&mut OsRng);
477
478    // Argon2 with default params (Argon2id v19)
479    let argon2 = Argon2::default();
480
481    // Hash password to PHC string ($argon2id$v=19$...)
482    let password_hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
483
484    Ok(password_hash)
485}