2025-03-12 12:32:15 -07:00
/*
* 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 < https ://www.gnu.org/licenses/>.
* /
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 < Regex > = 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 < String , FetchUserError > {
let user_id : Option < String > = 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 < String , FetchUserError > {
let user_id : Option < String > = 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 < String , CreateUserError > {
// 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 < bool , FetchUserError > {
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 < String > ,
pub admin : bool ,
pub verified : bool ,
}
/// gets all user info (:
pub async fn user_info ( db : & DatabaseConnection , id : String ) -> Result < UserInfo , FetchUserError > {
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 < Vec < String > , 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 < Vec < UserInfo > , 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 < usize , FetchUserError > {
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 < usize , FetchUserError > {
// dont fucking touch this, i don't know why it works but it does, it's actually evil
2025-03-19 20:58:23 -07:00
// note: doesn't work
2025-03-12 12:32:15 -07:00
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 < usize , FetchUserError > {
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 < Vec < ( String , String ) > , 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 ( ) )
2025-03-19 20:58:23 -07:00
}