diff --git a/crates/ezidam/src/routes/settings/personal.rs b/crates/ezidam/src/routes/settings/personal.rs index e7b7658..cecc22d 100644 --- a/crates/ezidam/src/routes/settings/personal.rs +++ b/crates/ezidam/src/routes/settings/personal.rs @@ -63,8 +63,20 @@ pub async fn user_settings_personal_form( } // Update username - if user.username() != form.username { - if let Err(e) = user.set_username(&mut transaction, form.username).await { + if user.username().0 != form.username { + // Parse username + let username = match Username::from_str(form.username) { + Ok(username) => username, + Err(_) => { + return Ok(Flash::new( + Redirect::to(uri!(user_settings_personal)), + FlashKind::Danger, + INVALID_USERNAME_ERROR, + )); + } + }; + + if let Err(e) = user.set_username(&mut transaction, &username).await { return Ok(Flash::new( Redirect::to(uri!(user_settings_personal)), FlashKind::Danger, diff --git a/crates/ezidam/src/routes/setup.rs b/crates/ezidam/src/routes/setup.rs index 8f473a5..383c87d 100644 --- a/crates/ezidam/src/routes/setup.rs +++ b/crates/ezidam/src/routes/setup.rs @@ -1,6 +1,7 @@ use super::prelude::*; use apps::App; use hash::{Secret, SecretString}; +use id::INVALID_USERNAME_ERROR; use rocket::{get, post}; use settings::Settings; use std::str::FromStr; @@ -38,6 +39,18 @@ async fn create_first_account( ) -> Result>> { let form = form.into_inner(); + // Parse username + let username = match Username::from_str(form.username) { + Ok(username) => username, + Err(_) => { + return Ok(Either::Right(Flash::new( + Redirect::to(uri!(setup)), + FlashKind::Danger, + INVALID_USERNAME_ERROR, + ))); + } + }; + // Parse url let url = match Url::parse(form.url) { Ok(url) => url, @@ -82,14 +95,7 @@ async fn create_first_account( .await?; // Insert user in database - User::insert( - &mut transaction, - &user_id, - true, - form.username, - Some(&password), - ) - .await?; + User::insert(&mut transaction, &user_id, true, &username, Some(&password)).await?; // Store UserID in settings Settings::set_first_admin(&mut transaction, &user_id).await?; diff --git a/crates/id/src/lib.rs b/crates/id/src/lib.rs index 3cbc4b3..0574ec6 100644 --- a/crates/id/src/lib.rs +++ b/crates/id/src/lib.rs @@ -1,6 +1,7 @@ mod app; mod key; mod user; +mod username; // error #[derive(thiserror::Error)] @@ -14,3 +15,5 @@ pub enum Error { pub use app::AppID; pub use key::KeyID; pub use user::UserID; +pub use username::Username; +pub use username::INVALID_USERNAME_ERROR; diff --git a/crates/id/src/username.rs b/crates/id/src/username.rs new file mode 100644 index 0000000..43d3e6f --- /dev/null +++ b/crates/id/src/username.rs @@ -0,0 +1,63 @@ +use super::Error; +use nanoid_dictionary::ALPHANUMERIC; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +pub const INVALID_USERNAME_ERROR: &str = "Invalid username. Pattern is [a-zA-Z0-9]"; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Username(pub String); + +impl Display for Username { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for Username { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl From<&Username> for String { + fn from(value: &Username) -> Self { + value.to_string() + } +} + +impl FromStr for Username { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.chars().all(|c| ALPHANUMERIC.contains(&c)) { + Ok(Self(s.to_string())) + } else { + Err(Error::Invalid("Username")) + } + } +} + +#[cfg(test)] +mod tests { + use super::Username; + use std::str::FromStr; + + #[test] + fn invalid_characters() { + let value = "#!👎 INVALID 🚫!#"; + + assert!(Username::from_str(value).is_err()); + } + + #[test] + fn valid_username() { + let value = "philt3r"; + let id = Username::from_str(value); + assert!(id.is_ok()); + + let id = id.unwrap(); + assert_eq!(id.0, value); + } +} diff --git a/crates/users/src/database.rs b/crates/users/src/database.rs index 39151bf..442803c 100644 --- a/crates/users/src/database.rs +++ b/crates/users/src/database.rs @@ -6,7 +6,7 @@ use database::Error as DatabaseError; use database::Users as DatabaseUsers; use email_address::EmailAddress; use hash::{PaperKey, Password, Secret}; -use id::UserID; +use id::{UserID, Username}; use std::str::FromStr; impl From for User { @@ -16,7 +16,7 @@ impl From for User { created_at: db.created_at, updated_at: db.updated_at, is_admin: db.is_admin, - username: db.username, + username: Username(db.username), name: db.name, email: db.email, password: db.password, @@ -41,13 +41,17 @@ impl User { conn: impl SqliteExecutor<'_>, id: &UserID, is_admin: bool, - username: &str, + username: &Username, password: Option<&Password>, ) -> Result, Error> { - Ok( - DatabaseUsers::insert(conn, &id.0, is_admin, username, password.map(|p| p.hash())) - .await?, + Ok(DatabaseUsers::insert( + conn, + &id.0, + is_admin, + username.as_ref(), + password.map(|p| p.hash()), ) + .await?) } pub async fn get_by_id( @@ -70,9 +74,9 @@ impl User { async fn get_by_username( conn: impl SqliteExecutor<'_>, - username: &str, + username: &Username, ) -> Result, Error> { - Ok(DatabaseUsers::get_one_by_username(conn, username) + Ok(DatabaseUsers::get_one_by_username(conn, username.as_ref()) .await? .map(Self::from)) } @@ -93,7 +97,11 @@ impl User { } // Get user from username - Self::get_by_username(conn, login).await + if let Ok(username) = Username::from_str(login) { + return Self::get_by_username(conn, &username).await; + } + + Err(Error::InvalidLogin(login.into())) } pub async fn get_one_from_authorization_code( @@ -136,9 +144,9 @@ impl User { pub async fn set_username( &self, conn: impl SqliteExecutor<'_>, - username: &str, + username: &Username, ) -> Result<(), Error> { - DatabaseUsers::set_username(conn, self.id.as_ref(), username) + DatabaseUsers::set_username(conn, self.id.as_ref(), username.as_ref()) .await .map_err(|e| match e { DatabaseError::UniqueConstraint(column) => { diff --git a/crates/users/src/error.rs b/crates/users/src/error.rs index 8fcff0a..249c38f 100644 --- a/crates/users/src/error.rs +++ b/crates/users/src/error.rs @@ -14,4 +14,7 @@ pub enum Error { #[error("The email \"{0}\" is linked to another user.")] EmailNotAvailable(String), + + #[error("The login \"{0}\" is not valid.")] + InvalidLogin(String), } diff --git a/crates/users/src/lib.rs b/crates/users/src/lib.rs index cca2b81..22a7f24 100644 --- a/crates/users/src/lib.rs +++ b/crates/users/src/lib.rs @@ -4,7 +4,7 @@ pub mod password_reset; pub mod totp_login_request; use chrono::{DateTime, Utc}; -use id::UserID; +use id::{UserID, Username}; use serde::Serialize; pub use crate::error::Error; @@ -16,7 +16,7 @@ pub struct User { created_at: DateTime, updated_at: DateTime, is_admin: bool, - username: String, + username: Username, name: Option, email: Option, password: Option, @@ -38,7 +38,7 @@ impl User { pub fn password_hashed(&self) -> Option<&str> { self.password.as_deref() } - pub fn username(&self) -> &str { + pub fn username(&self) -> &Username { &self.username } pub fn name(&self) -> Option<&str> {