username: type to parse username

This commit is contained in:
Philippe Loctaux 2023-05-04 22:47:11 +02:00
parent 0e77f7be5e
commit bdd5eca9f1
7 changed files with 119 additions and 24 deletions

View file

@ -63,8 +63,20 @@ pub async fn user_settings_personal_form(
} }
// Update username // Update username
if user.username() != form.username { if user.username().0 != form.username {
if let Err(e) = user.set_username(&mut transaction, form.username).await { // 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( return Ok(Flash::new(
Redirect::to(uri!(user_settings_personal)), Redirect::to(uri!(user_settings_personal)),
FlashKind::Danger, FlashKind::Danger,

View file

@ -1,6 +1,7 @@
use super::prelude::*; use super::prelude::*;
use apps::App; use apps::App;
use hash::{Secret, SecretString}; use hash::{Secret, SecretString};
use id::INVALID_USERNAME_ERROR;
use rocket::{get, post}; use rocket::{get, post};
use settings::Settings; use settings::Settings;
use std::str::FromStr; use std::str::FromStr;
@ -38,6 +39,18 @@ async fn create_first_account(
) -> Result<Either<Redirect, Flash<Redirect>>> { ) -> Result<Either<Redirect, Flash<Redirect>>> {
let form = form.into_inner(); 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 // Parse url
let url = match Url::parse(form.url) { let url = match Url::parse(form.url) {
Ok(url) => url, Ok(url) => url,
@ -82,14 +95,7 @@ async fn create_first_account(
.await?; .await?;
// Insert user in database // Insert user in database
User::insert( User::insert(&mut transaction, &user_id, true, &username, Some(&password)).await?;
&mut transaction,
&user_id,
true,
form.username,
Some(&password),
)
.await?;
// Store UserID in settings // Store UserID in settings
Settings::set_first_admin(&mut transaction, &user_id).await?; Settings::set_first_admin(&mut transaction, &user_id).await?;

View file

@ -1,6 +1,7 @@
mod app; mod app;
mod key; mod key;
mod user; mod user;
mod username;
// error // error
#[derive(thiserror::Error)] #[derive(thiserror::Error)]
@ -14,3 +15,5 @@ pub enum Error {
pub use app::AppID; pub use app::AppID;
pub use key::KeyID; pub use key::KeyID;
pub use user::UserID; pub use user::UserID;
pub use username::Username;
pub use username::INVALID_USERNAME_ERROR;

63
crates/id/src/username.rs Normal file
View file

@ -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<str> 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<Self, Self::Err> {
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);
}
}

View file

@ -6,7 +6,7 @@ use database::Error as DatabaseError;
use database::Users as DatabaseUsers; use database::Users as DatabaseUsers;
use email_address::EmailAddress; use email_address::EmailAddress;
use hash::{PaperKey, Password, Secret}; use hash::{PaperKey, Password, Secret};
use id::UserID; use id::{UserID, Username};
use std::str::FromStr; use std::str::FromStr;
impl From<DatabaseUsers> for User { impl From<DatabaseUsers> for User {
@ -16,7 +16,7 @@ impl From<DatabaseUsers> for User {
created_at: db.created_at, created_at: db.created_at,
updated_at: db.updated_at, updated_at: db.updated_at,
is_admin: db.is_admin, is_admin: db.is_admin,
username: db.username, username: Username(db.username),
name: db.name, name: db.name,
email: db.email, email: db.email,
password: db.password, password: db.password,
@ -41,13 +41,17 @@ impl User {
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
id: &UserID, id: &UserID,
is_admin: bool, is_admin: bool,
username: &str, username: &Username,
password: Option<&Password>, password: Option<&Password>,
) -> Result<Option<()>, Error> { ) -> Result<Option<()>, Error> {
Ok( Ok(DatabaseUsers::insert(
DatabaseUsers::insert(conn, &id.0, is_admin, username, password.map(|p| p.hash())) conn,
.await?, &id.0,
is_admin,
username.as_ref(),
password.map(|p| p.hash()),
) )
.await?)
} }
pub async fn get_by_id( pub async fn get_by_id(
@ -70,9 +74,9 @@ impl User {
async fn get_by_username( async fn get_by_username(
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
username: &str, username: &Username,
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, Error> {
Ok(DatabaseUsers::get_one_by_username(conn, username) Ok(DatabaseUsers::get_one_by_username(conn, username.as_ref())
.await? .await?
.map(Self::from)) .map(Self::from))
} }
@ -93,7 +97,11 @@ impl User {
} }
// Get user from username // 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( pub async fn get_one_from_authorization_code(
@ -136,9 +144,9 @@ impl User {
pub async fn set_username( pub async fn set_username(
&self, &self,
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
username: &str, username: &Username,
) -> Result<(), Error> { ) -> Result<(), Error> {
DatabaseUsers::set_username(conn, self.id.as_ref(), username) DatabaseUsers::set_username(conn, self.id.as_ref(), username.as_ref())
.await .await
.map_err(|e| match e { .map_err(|e| match e {
DatabaseError::UniqueConstraint(column) => { DatabaseError::UniqueConstraint(column) => {

View file

@ -14,4 +14,7 @@ pub enum Error {
#[error("The email \"{0}\" is linked to another user.")] #[error("The email \"{0}\" is linked to another user.")]
EmailNotAvailable(String), EmailNotAvailable(String),
#[error("The login \"{0}\" is not valid.")]
InvalidLogin(String),
} }

View file

@ -4,7 +4,7 @@ pub mod password_reset;
pub mod totp_login_request; pub mod totp_login_request;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use id::UserID; use id::{UserID, Username};
use serde::Serialize; use serde::Serialize;
pub use crate::error::Error; pub use crate::error::Error;
@ -16,7 +16,7 @@ pub struct User {
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
is_admin: bool, is_admin: bool,
username: String, username: Username,
name: Option<String>, name: Option<String>,
email: Option<String>, email: Option<String>,
password: Option<String>, password: Option<String>,
@ -38,7 +38,7 @@ impl User {
pub fn password_hashed(&self) -> Option<&str> { pub fn password_hashed(&self) -> Option<&str> {
self.password.as_deref() self.password.as_deref()
} }
pub fn username(&self) -> &str { pub fn username(&self) -> &Username {
&self.username &self.username
} }
pub fn name(&self) -> Option<&str> { pub fn name(&self) -> Option<&str> {