/* * 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 // note: doesn't work 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()) }