use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use sqlx::{error::DatabaseError, Sqlite, SqlitePool}; use tracing::log::log; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; const CREATE_QUERY: &str = "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; pub struct User { id: Uuid, username: String, displayname: Option, email: Option, } #[derive(Debug, Clone, sqlx::FromRow, sqlx::Encode)] pub(crate) struct DbUser { id: Uuid, username: String, displayname: Option, email: Option, last_seen: Option, pwhash: String, } impl From for User { fn from(dbu: DbUser) -> Self { User { id: dbu.id, username: dbu.username, displayname: dbu.displayname, email: dbu.email, } } } pub async fn create_user( username: &str, displayname: &Option, email: &Option, password: &[u8], pool: &SqlitePool, ) -> Result { let username = username.trim(); let name_len = username.graphemes(true).size_hint().1.unwrap(); // we are not ascii exclusivists around here if !(1..=20).contains(&name_len) { return Err(CreateUserErrorKind::BadUsername.into()); } // Argon2 with default params (Argon2id v19) let argon2 = Argon2::default(); let salt = SaltString::generate(&mut OsRng); let pwhash = argon2 .hash_password(password, &salt) .unwrap() // safe to unwrap, we know the salt is valid .to_string(); let id = Uuid::new_v4(); let id_bytes = id.to_bytes_le(); let id_bytes = id_bytes.as_slice(); let res = sqlx::query(CREATE_QUERY) .bind(id_bytes) .bind(username) .bind(displayname) .bind(email) .bind(pwhash) .execute(pool) .await; match res { Ok(_) => { let user = User { id, username: username.to_string(), displayname: displayname.to_owned(), email: email.to_owned(), }; Ok(user) } Err(sqlx::Error::Database(db)) => { if let Some(exit) = db.code() { let exit = exit.parse().unwrap_or(0u32); // https://www.sqlite.org/rescode.html codes for unique constraint violations: if exit == 2067u32 || exit == 1555 { Err(CreateUserErrorKind::AlreadyExists.into()) } else { Err(CreateUserErrorKind::UnknownDBError.into()) } } else { Err(CreateUserErrorKind::UnknownDBError.into()) } } _ => Err(CreateUserErrorKind::UnknownDBError.into()), } } #[Error(desc = "Could not create user.")] #[non_exhaustive] pub struct CreateUserError(#[from] CreateUserErrorKind); #[Error] #[non_exhaustive] pub enum CreateUserErrorKind { AlreadyExists, #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] BadUsername, PasswordMismatch, MissingFields, UnknownDBError, }