2023-05-12 21:24:57 +00:00
|
|
|
use argon2::{
|
|
|
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
|
|
Argon2,
|
|
|
|
};
|
2023-05-15 03:33:36 +00:00
|
|
|
use sqlx::{error::DatabaseError, Sqlite, SqlitePool};
|
|
|
|
use tracing::log::log;
|
2023-05-12 21:24:57 +00:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
2023-05-15 03:33:36 +00:00
|
|
|
const CREATE_QUERY: &str =
|
|
|
|
"insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)";
|
2023-05-12 22:36:19 +00:00
|
|
|
|
|
|
|
pub struct User {
|
|
|
|
id: Uuid,
|
|
|
|
username: String,
|
|
|
|
displayname: Option<String>,
|
|
|
|
email: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, sqlx::FromRow, sqlx::Encode)]
|
2023-05-16 18:55:32 +00:00
|
|
|
pub(crate) struct DbUser {
|
2023-05-12 22:36:19 +00:00
|
|
|
id: Uuid,
|
|
|
|
username: String,
|
|
|
|
displayname: Option<String>,
|
|
|
|
email: Option<String>,
|
|
|
|
last_seen: Option<u64>,
|
|
|
|
pwhash: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<DbUser> for User {
|
|
|
|
fn from(dbu: DbUser) -> Self {
|
|
|
|
User {
|
|
|
|
id: dbu.id,
|
|
|
|
username: dbu.username,
|
|
|
|
displayname: dbu.displayname,
|
|
|
|
email: dbu.email,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 03:33:36 +00:00
|
|
|
pub async fn create_user(
|
|
|
|
username: &str,
|
|
|
|
displayname: &Option<String>,
|
|
|
|
email: &Option<String>,
|
|
|
|
password: &[u8],
|
|
|
|
pool: &SqlitePool,
|
|
|
|
) -> Result<User, CreateUserError> {
|
2023-05-12 21:24:57 +00:00
|
|
|
// Argon2 with default params (Argon2id v19)
|
|
|
|
let argon2 = Argon2::default();
|
|
|
|
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
2023-05-15 03:33:36 +00:00
|
|
|
let pwhash = argon2
|
2023-05-12 21:24:57 +00:00
|
|
|
.hash_password(password, &salt)
|
|
|
|
.unwrap() // safe to unwrap, we know the salt is valid
|
|
|
|
.to_string();
|
|
|
|
|
2023-05-15 03:33:36 +00:00
|
|
|
let id = Uuid::new_v4();
|
2023-05-16 18:55:32 +00:00
|
|
|
let id_bytes = id.as_bytes().as_slice();
|
2023-05-15 03:33:36 +00:00
|
|
|
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() {
|
2023-05-16 18:55:32 +00:00
|
|
|
let exit = exit.parse().unwrap_or(0u32);
|
|
|
|
// https://www.sqlite.org/rescode.html codes for unique constraint violations:
|
|
|
|
if exit == 2067u32 || exit == 1555 {
|
2023-05-15 03:33:36 +00:00
|
|
|
Err(CreateUserErrorKind::AlreadyExists.into())
|
|
|
|
} else {
|
|
|
|
Err(CreateUserErrorKind::Unknown.into())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(CreateUserErrorKind::Unknown.into())
|
|
|
|
}
|
|
|
|
}
|
2023-05-16 18:55:32 +00:00
|
|
|
_ => Err(CreateUserErrorKind::Unknown.into()),
|
2023-05-15 03:33:36 +00:00
|
|
|
}
|
2023-05-12 21:24:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[Error(desc = "Could not create user.")]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub struct CreateUserError(#[from] CreateUserErrorKind);
|
|
|
|
|
|
|
|
#[Error]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum CreateUserErrorKind {
|
|
|
|
AlreadyExists,
|
2023-05-16 18:55:32 +00:00
|
|
|
#[error(desc = "Usernames must be less than 20 characters long")]
|
2023-05-15 03:33:36 +00:00
|
|
|
BadUsername,
|
2023-05-12 21:24:57 +00:00
|
|
|
PasswordMismatch,
|
|
|
|
MissingFields,
|
|
|
|
Unknown,
|
|
|
|
}
|