/*
* authservice db/user.rs
* - pgsql user functions
*
* Copyright (C) 2025 Real Microsoft, LLC
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
*/
use argon2::{Argon2, PasswordHasher, PasswordVerifier};
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use chrono::{Days, Utc};
use futures::FutureExt;
use log::{debug, error};
use once_cell::sync::Lazy;
use regex::Regex;
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, ModelTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
use sea_orm::ActiveValue::Set;
use sea_orm::prelude::Expr;
use sea_orm::sea_query::{BinOper, ExprTrait};
use entity::{user, verification_code};
use crate::db::genid;
static EMAIL_REGEX: Lazy = Lazy::new(|| Regex::new(r#"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"#).expect("BAD EMAIL REGEX"));
#[repr(i64)]
pub enum UserFlag {
Verified = 1 << 0,
NewEmail = 1 << 1,
Administrator = 1 << 2,
}
#[derive(Debug)]
pub enum CreateUserError {
/// user already exists with this username
UsernameTaken,
/// user already exists with this email
EmailTaken,
/// username contains invalid (not alphanumeric with dashes and underscores allowed) characters, or is arbitrarily too long
UsernameError,
/// display name contains invalid (not alphanumeric with dashes and underscores and spaces (but not at the beginning or end, or twice in a row) characters, or is arbitrarily too long
DisplayNameError,
/// email doesn't match funny email regex
EmailError,
/// password weak (not implemented yet)
PasswordError,
/// arbitrary database error, check logs
DatabaseError,
}
#[derive(Debug)]
pub enum FetchUserError {
UserNotFound,
DatabaseError,
}
/// takes a user id and returns Ok if the user exists
pub async fn check_if_user_exists(db: &DatabaseConnection, user_id: String) -> Result<(), FetchUserError> {
if user::Entity::find_by_id(&user_id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE USER EXIST CHECK: {e}");
FetchUserError::DatabaseError
})?.is_none() {
return Err(FetchUserError::UserNotFound);
}
Ok(())
}
/// takes a username and returns the associated user id, or an error if the user does not exist
pub async fn username_to_userid(db: &DatabaseConnection, username: String) -> Result {
let user_id: Option = user::Entity::find().filter(user::Column::Username.eq(&username)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE USERNAME2USERID: {e}");
FetchUserError::DatabaseError
})?.map(|v| v.id);
if user_id.is_none() {
return Err(FetchUserError::UserNotFound);
}
Ok(user_id.unwrap())
}
/// takes an email and returns the associated user id, or an error if the user does not exist
pub async fn email_to_userid(db: &DatabaseConnection, email: String) -> Result {
let user_id: Option = user::Entity::find().filter(user::Column::Email.eq(&email)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE EMAIL2USERID: {e}");
FetchUserError::DatabaseError
})?.map(|v| v.id);
if user_id.is_none() {
return Err(FetchUserError::UserNotFound);
}
Ok(user_id.unwrap())
}
/// creates a user in the database, you'll probably want to register an email verification code after this
/// returns id on success
pub async fn create_user(db: &DatabaseConnection, username: String, display_name: String, email: String, password: String) -> Result {
// rustrover gives some warnings here but don't change the regex because it's directly
// taken from the html standard
if !EMAIL_REGEX.is_match(&email) {
return Err(CreateUserError::EmailError);
}
if username.len() > 32 {
return Err(CreateUserError::UsernameError);
}
if user::Entity::find().filter(user::Column::Username.eq(&username)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CREATE USER (username taken check (database)): {e}");
CreateUserError::DatabaseError
})?.is_some() {
return Err(CreateUserError::UsernameTaken);
}
if user::Entity::find().filter(user::Column::Email.eq(&email)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CREATE USER (email taken check (database)): {e}");
CreateUserError::DatabaseError
})?.is_some() {
return Err(CreateUserError::EmailTaken);
}
let id = genid();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2.hash_password(password.as_bytes(), &salt)
.expect("argon2 error").to_string();
let now = Utc::now();
let record = user::ActiveModel {
id: Set(id.clone()),
username: Set(username),
display_name: Set(Some(display_name)),
email: Set(email),
new_email: Set(None),
password: Set(password_hash),
flags: Set(0),
created_on: Set(now.naive_utc()),
last_updated: Set(now.naive_utc()),
};
user::Entity::insert(record).exec(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CREATE USER (insert user): {e}");
CreateUserError::DatabaseError
})?;
Ok(id)
}
#[derive(Debug)]
pub enum DeleteUserError {
UserDoesntExist,
DatabaseError,
}
/// deletes the given user from the user table, doesn't do any other cleanup so be careful!
/// returns an error if the user doesn't exist
pub async fn delete_user(db: &DatabaseConnection, id: String) -> Result<(), DeleteUserError> {
let record = user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE DELETEUSER (exist check (database)): {e}");
DeleteUserError::DatabaseError
})?;
if record.is_none() {
return Err(DeleteUserError::UserDoesntExist);
}
record.unwrap().delete(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE DELETEUSER (delete (database)): {e}");
DeleteUserError::DatabaseError
})?;
Ok(())
}
#[derive(Debug)]
pub enum RegisterVerificationCodeError {
/// user is already verified, and thus we do not need a new verification code
/// note that for email changes, we use the new_verified field which is true until a new email is set
UserIsVerifiedAndNotChangingTheirEmail,
/// user seemingly doesn't exist in the database, this should be
/// shown to the user as an invalid session error, or an internal server error, depending
/// on the situation
UserDoesntExist,
DatabaseError,
}
/// inserts a new email verification code into the user flags for this user,
/// user is verified after you verify_verification_code
pub async fn register_verification_code(db: &DatabaseConnection, id: String, verification_code: String) -> Result<(), RegisterVerificationCodeError> {
let record = user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE REGISTERVERIFICATIONCODE (record for flags check (query)): {e}");
RegisterVerificationCodeError::DatabaseError
})?.ok_or(RegisterVerificationCodeError::UserDoesntExist)?;
// if verified and not asking for new email
if record.flags & UserFlag::Verified as i64 != 0 && record.flags & UserFlag::NewEmail as i64 == 0 {
return Err(RegisterVerificationCodeError::UserIsVerifiedAndNotChangingTheirEmail);
}
let vc_record = verification_code::ActiveModel {
id: Set(genid()),
user: Set(id.clone()),
code: Set(verification_code),
set_new_email: Set(false),
created_on: Set(Utc::now().naive_utc()),
death_date: Set(Utc::now().checked_add_days(Days::new(10)).unwrap().naive_utc()),
used: Set(false),
used_on: NotSet,
};
verification_code::Entity::insert(vc_record).exec(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE REGISTERVERIFICATIONCODE (insert verification code): {e}");
RegisterVerificationCodeError::DatabaseError
})?;
Ok(())
}
#[derive(Debug)]
pub enum VerifyVerificationCodeError {
UserAlreadyVerified,
VerificationCodeInvalid,
UserDoesntExist,
DatabaseError,
}
/// verifies that the verification code is correct, and sets the user as verified if so
pub async fn verify_verification_code(db: &DatabaseConnection, id: String, verification_code: String) -> Result<(), VerifyVerificationCodeError> {
let vc_record = verification_code::Entity::find().filter(verification_code::Column::Code.eq(&verification_code)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE VERIFYVERIFICATIONCODE (code record select): {e}");
VerifyVerificationCodeError::DatabaseError
})?.ok_or(VerifyVerificationCodeError::VerificationCodeInvalid)?;
if vc_record.user != id {
return Err(VerifyVerificationCodeError::UserDoesntExist);
}
let user_record = user::Entity::find_by_id(&vc_record.user).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE VERIFYVERIFICATIONCODE (user record select): {e}");
VerifyVerificationCodeError::DatabaseError
})?.ok_or(VerifyVerificationCodeError::UserDoesntExist)?;
// if verified and not asking for new email
if user_record.flags & UserFlag::Verified as i64 != 0 && user_record.flags & UserFlag::NewEmail as i64 == 0 {
return Err(VerifyVerificationCodeError::UserAlreadyVerified);
}
let mut user_record = user_record.into_active_model();
user_record.flags = Set((*user_record.flags.as_ref() | UserFlag::Verified as i64) & !(UserFlag::NewEmail as i64));
if vc_record.set_new_email {
if let Some(email) = user_record.new_email.as_ref() {
user_record.email = Set(email.clone());
user_record.new_email = Set(None);
} else {
error!("vc record is SetNewEmail, but no new email exists for this user {id}!!! INVESTIGATE ASAP");
}
}
let mut vc_record = vc_record.into_active_model();
vc_record.used = Set(true);
vc_record.used_on = Set(Some(Utc::now().naive_utc()));
user_record.update(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE VERIFYVERIFICATIONCODE (user record update): {e}");
VerifyVerificationCodeError::DatabaseError
})?;
vc_record.update(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE VERIFYVERIFICATIONCODE (vc record update): {e}");
VerifyVerificationCodeError::DatabaseError
})?;
Ok(())
}
/// checks if the user can log-in with their current registration status
pub async fn check_if_verified(db: &DatabaseConnection, id: String) -> Result {
let user_record = user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CHECKVERIFICATION (user record select): {e}");
FetchUserError::DatabaseError
})?.ok_or(FetchUserError::UserNotFound)?;
Ok(user_record.flags & UserFlag::Verified as i64 != 0)
}
#[derive(Debug)]
pub enum VerifyEmailPassComboError {
ComboInvalid,
UserDoesntExist,
DatabaseError,
}
/// verifies that the given email/pass combo are correct for the account associated with the email
/// password must be unhashed
pub async fn verify_email_pass_combo(db: &DatabaseConnection, email: String, password: String) -> Result<(), VerifyEmailPassComboError> {
let password_hash = user::Entity::find().filter(user::Column::Email.eq(&email)).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE VERIFYEMAILPASSCOMBO (password hash query): {e}");
VerifyEmailPassComboError::DatabaseError
})?.ok_or(VerifyEmailPassComboError::UserDoesntExist)?.password;
let password_hash = argon2::PasswordHash::new(&password_hash).map_err(|e| {
error!("invalid password hash found for {email}! check database for corruption! argon2 error: {e}");
VerifyEmailPassComboError::DatabaseError
})?;
if Argon2::default().verify_password(password.as_bytes(), &password_hash).is_err() {
return Err(VerifyEmailPassComboError::ComboInvalid);
}
Ok(())
}
#[derive(Debug)]
pub struct UserInfo {
pub username: String,
pub email: String,
pub new_email: Option,
pub admin: bool,
pub verified: bool,
}
/// gets all user info (:
pub async fn user_info(db: &DatabaseConnection, id: String) -> Result {
user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE USERINFO: {e}");
FetchUserError::DatabaseError
})?.ok_or(FetchUserError::UserNotFound).map(|v| {
UserInfo {
username: v.username,
email: v.email,
new_email: v.new_email,
admin: v.flags & UserFlag::Administrator as i64 != 0,
verified: v.flags & UserFlag::Verified as i64 != 0,
}
})
}
#[derive(Debug)]
pub enum EmailChangeError {
UserNotFound,
NewEmailIsInvalid,
DatabaseError,
}
/// starts the process for changing a user's email
pub async fn start_email_change(db: &DatabaseConnection, id: String, new_email: String, verification_code: String) -> Result<(), EmailChangeError> {
if !EMAIL_REGEX.is_match(&new_email) {
return Err(EmailChangeError::NewEmailIsInvalid);
}
let user_record = user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE STARTEMAILCHANGE (user query): {e}");
EmailChangeError::DatabaseError
})?.ok_or(EmailChangeError::UserNotFound)?;
let flags = user_record.flags;
let mut user_record = user_record.into_active_model();
user_record.new_email = Set(Some(new_email));
user_record.flags = Set(flags | UserFlag::NewEmail as i64);
user_record.update(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE STARTEMAILCHANGE (user update): {e}");
EmailChangeError::DatabaseError
})?;
let old_vc_records = verification_code::Entity::delete_many()
.filter(verification_code::Column::User.eq(&id))
.filter(verification_code::Column::SetNewEmail.eq(true))
.exec(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE STARTEMAILCHANGE (delete old email codes): {e}");
EmailChangeError::DatabaseError
})?;
debug!("deleted {} old vc code records during email change", old_vc_records.rows_affected);
let vc_record = verification_code::ActiveModel {
id: Set(genid()),
user: Set(id.clone()),
code: Set(verification_code),
set_new_email: Set(true),
created_on: Set(Utc::now().naive_utc()),
death_date: Set(Utc::now().checked_add_days(Days::new(10)).unwrap().naive_utc()),
used: Set(false),
used_on: NotSet,
};
verification_code::Entity::insert(vc_record).exec(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE STARTEMAILCHANGE (insert verification code): {e}");
EmailChangeError::DatabaseError
})?;
Ok(())
}
/// cancels any in-progress user email change
pub async fn cancel_email_change(db: &DatabaseConnection, id: String) -> Result<(), FetchUserError> {
verification_code::Entity::delete_many()
.filter(verification_code::Column::User.eq(&id))
.filter(verification_code::Column::SetNewEmail.eq(true))
.exec(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CANCELEMAILCHANGE (delete old email codes): {e}");
FetchUserError::DatabaseError
})?;
let mut user_record = user::Entity::find_by_id(&id).one(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CANCELEMAILCHANGE (query user): {e}");
FetchUserError::DatabaseError
})?.ok_or(FetchUserError::UserNotFound)?.into_active_model();
let flags = *user_record.flags.as_ref();
user_record.new_email = Set(None);
// unset new email
user_record.flags = Set(flags & !(UserFlag::NewEmail as i64));
user_record.update(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE CANCELEMAILCHANGE (user update): {e}");
FetchUserError::DatabaseError
})?;
Ok(())
}
/// returns the last 10 users added to the database as usernames
pub async fn last_ten_usernames(db: &DatabaseConnection) -> Result, FetchUserError> {
Ok(user::Entity::find()
.order_by_desc(user::Column::CreatedOn)
.limit(Some(10))
.all(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE LASTTENUSERNAMES: {e}");
FetchUserError::DatabaseError
})?.into_iter().map(|v| v.username).collect())
}
pub async fn all_users(db: &DatabaseConnection) -> Result, FetchUserError> {
Ok(user::Entity::find().all(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE ALLUSERS: {e}");
FetchUserError::DatabaseError
})?.into_iter().map(|v| {
UserInfo {
username: v.username,
email: v.email,
new_email: v.new_email,
admin: v.flags & UserFlag::Administrator as i64 != 0,
verified: v.flags & UserFlag::Verified as i64 != 0,
}
}).collect())
}
/// returns the number of users in the database
pub async fn user_count(db: &DatabaseConnection) -> Result {
Ok(user::Entity::find().count(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE USERCOUNT: {e}");
FetchUserError::DatabaseError
})? as usize)
}
/// returns the number of users in the database who are admins
pub async fn admin_count(db: &DatabaseConnection) -> Result {
// dont fucking touch this, i don't know why it works but it does, it's actually evil
Ok(user::Entity::find().filter(user::Column::Flags.into_expr().binary(BinOper::LShift, Expr::value(63 - 2)).lt(1 << (63 - 2)))
.count(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE ADMINCOUNT: {e}");
FetchUserError::DatabaseError
})? as usize)
}
/// returns the number of unverified accounts (this does NOT count email change requests)
pub async fn unverified_count(db: &DatabaseConnection) -> Result {
Ok(verification_code::Entity::find().filter(verification_code::Column::SetNewEmail.eq(false)).filter(verification_code::Column::Used.eq(false))
.count(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE UNVERIFIEDCOUNT: {e}");
FetchUserError::DatabaseError
})? as usize)
}
/// returns a list of all (username, email) to send announcement emails to
pub async fn email_list(db: &DatabaseConnection) -> Result, FetchUserError> {
// fixme: filter by who wants emails and who doesn't when we implement that
Ok(user::Entity::find().all(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE EMAILLIST: {e}");
FetchUserError::DatabaseError
})?.into_iter().map(|v| {
(v.username, v.email)
}).collect())
}