2025-03-12 12:32:15 -07:00
/*
* authservice process / basic_accounts . rs
* - basic authentication + creation for accounts , as well as a few other things
*
* 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 std ::future ::Future ;
use asklyphe_common ::nats ::authservice ::{ AuthError , AuthRequest , AuthResponse , AuthServiceResponse , CancelEmailChangeRequest , CancelEmailChangeResponse , EmailChangeRequest , EmailChangeResponse , EmailError , LoginError , LoginRequest , LoginResponse , LogoutRequest , LogoutResponse , PasswordError , RegisterError , RegisterRequest , RegisterResponse , TokenAssignment , UsernameError , VerifyEmailRequest , VerifyEmailResponse } ;
use chrono ::{ DateTime , Utc } ;
use log ::{ debug , error } ;
use rand ::Rng ;
use sea_orm ::DatabaseConnection ;
use crate ::db ::{ invite_code , session , user } ;
use crate ::db ::invite_code ::ConsumeInviteCodeError ;
use crate ::db ::session ::{ CreateSessionError , DeleteSessionError , FetchSessionError } ;
use crate ::db ::user ::{ CreateUserError , DeleteUserError , EmailChangeError , FetchUserError , RegisterVerificationCodeError , VerifyEmailPassComboError , VerifyVerificationCodeError } ;
2025-03-20 16:59:31 -07:00
use crate ::{ email , AUTH_URL } ;
2025-03-12 12:32:15 -07:00
fn generate_email_verification_code ( ) -> String {
rand ::thread_rng ( )
. sample_iter ( & rand ::distributions ::Alphanumeric )
. take ( 16 )
. map ( char ::from )
. collect ( )
}
pub async fn register_request ( db : & DatabaseConnection , request : RegisterRequest ) -> RegisterResponse {
// goes something like:
// 1. create user in db
// 2. try to consume invite code
// 2.5. if that fails, delete user from db
// 3. otherwise, register email verification code in db
// 4. this is where we'd send emails out with the verification code, if we had that set up...
let user = user ::create_user (
db ,
request . username . clone ( ) ,
request . username . clone ( ) ,
request . email . clone ( ) ,
request . password ) . await ;
if let Err ( e ) = user {
return match e {
CreateUserError ::UsernameTaken = > {
RegisterResponse ::Failure ( RegisterError ::Username ( UsernameError ::Taken ) )
}
CreateUserError ::EmailTaken = > {
RegisterResponse ::Failure ( RegisterError ::Email ( EmailError ::EmailTaken ) )
}
CreateUserError ::UsernameError = > {
RegisterResponse ::Failure ( RegisterError ::Username ( UsernameError ::InvalidCharacters ) )
}
CreateUserError ::DisplayNameError = > {
unimplemented! ( " we don't explicitly allow display names to be set, username error would be hit before display name error " )
}
CreateUserError ::EmailError = > {
RegisterResponse ::Failure ( RegisterError ::Email ( EmailError ::InvalidEmail ) )
}
CreateUserError ::PasswordError = > {
// todo: actual password errors that tell you why it's bad
RegisterResponse ::Failure ( RegisterError ::Password ( PasswordError ::TooShort ) )
}
CreateUserError ::DatabaseError = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " database error " . to_string ( ) ) )
}
} ;
}
let user_id = user . unwrap ( ) ;
if let Err ( e ) = invite_code ::consume_invite_code ( db , request . invite_code . clone ( ) , user_id . clone ( ) ) . await {
if let Err ( e ) = user ::delete_user ( db , user_id . clone ( ) ) . await {
return match e {
DeleteUserError ::UserDoesntExist = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " unexpected missing user " . to_string ( ) ) )
}
DeleteUserError ::DatabaseError = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " database error " . to_string ( ) ) )
}
} ;
}
return match e {
ConsumeInviteCodeError ::InvalidUser = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " unexpected missing user " . to_string ( ) ) )
}
ConsumeInviteCodeError ::DatabaseError = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " database error " . to_string ( ) ) )
}
_ = > {
RegisterResponse ::Failure ( RegisterError ::BadInviteCode )
}
} ;
}
let verification_code = generate_email_verification_code ( ) ;
if let Err ( e ) = user ::register_verification_code ( db , user_id . clone ( ) , verification_code . clone ( ) ) . await {
if let Err ( e ) = user ::delete_user ( db , user_id . clone ( ) ) . await {
match e {
DeleteUserError ::UserDoesntExist = > {
error! ( " failed to register verify code during signup, and couldn't delete the user because they don't exist? " )
}
DeleteUserError ::DatabaseError = > {
error! ( " database error while trying to delete user after registering verify code failed during signup " )
}
}
}
return match e {
RegisterVerificationCodeError ::UserIsVerifiedAndNotChangingTheirEmail = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " unexpected corrupt user struct " . to_string ( ) ) )
}
RegisterVerificationCodeError ::UserDoesntExist = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " unexpected missing user " . to_string ( ) ) )
}
RegisterVerificationCodeError ::DatabaseError = > {
RegisterResponse ::Failure ( RegisterError ::InternalServer ( " database error " . to_string ( ) ) )
}
} ;
}
debug! ( " verification code for {} is \" {} \" " , request . username , verification_code ) ;
2025-03-20 16:59:31 -07:00
email ::send_verification_code_email ( & request . email , & request . username , format! ( " {} /verify?username= {} &token= {} " , AUTH_URL . as_str ( ) , request . username , verification_code ) . as_str ( ) ) ;
2025-03-12 12:32:15 -07:00
RegisterResponse ::Success
}
pub async fn verify_email ( db : & DatabaseConnection , request : VerifyEmailRequest ) -> VerifyEmailResponse {
let user_id = user ::username_to_userid ( db , request . username ) . await ;
if let Err ( e ) = user_id {
return match e {
FetchUserError ::UserNotFound = > {
VerifyEmailResponse ::InvalidToken
}
FetchUserError ::DatabaseError = > {
VerifyEmailResponse ::InternalServerError ( " database error during username2userid " . to_string ( ) )
}
} ;
}
let user_id = user_id . unwrap ( ) ;
if let Err ( e ) = user ::verify_verification_code ( db , user_id , request . token ) . await {
return match e {
VerifyVerificationCodeError ::UserAlreadyVerified = > {
VerifyEmailResponse ::InvalidToken
}
VerifyVerificationCodeError ::VerificationCodeInvalid = > {
VerifyEmailResponse ::InvalidToken
}
VerifyVerificationCodeError ::UserDoesntExist = > {
VerifyEmailResponse ::InvalidToken
}
VerifyVerificationCodeError ::DatabaseError = > {
VerifyEmailResponse ::InternalServerError ( " database error during username2userid " . to_string ( ) )
}
} ;
}
VerifyEmailResponse ::Success
}
pub async fn login ( db : & DatabaseConnection , request : LoginRequest ) -> LoginResponse {
// 1. verify that credentials are correct
// 2. create session
// 3. return token
if let Err ( e ) = user ::verify_email_pass_combo ( db , request . email . clone ( ) , request . password ) . await {
return match e {
VerifyEmailPassComboError ::ComboInvalid = > {
LoginResponse ::Failure ( LoginError ::InvalidAccount )
}
VerifyEmailPassComboError ::UserDoesntExist = > {
LoginResponse ::Failure ( LoginError ::InvalidAccount )
}
VerifyEmailPassComboError ::DatabaseError = > {
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while verifying email/pass combo " . to_string ( ) ) )
}
} ;
}
let user_id = match user ::email_to_userid ( db , request . email ) . await {
Ok ( user_id ) = > { user_id }
Err ( e ) = > {
return match e {
FetchUserError ::UserNotFound = > {
LoginResponse ::Failure ( LoginError ::InvalidAccount )
}
FetchUserError ::DatabaseError = > {
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while converting email to userid " . to_string ( ) ) )
}
} ;
}
} ;
match user ::check_if_verified ( db , user_id . clone ( ) ) . await {
Ok ( b ) = > {
if ! b {
return LoginResponse ::Failure ( LoginError ::AccountNotVerified ) ;
}
}
Err ( e ) = > {
return match e {
FetchUserError ::UserNotFound = > {
LoginResponse ::Failure ( LoginError ::InvalidAccount )
}
FetchUserError ::DatabaseError = > {
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while checking verification status " . to_string ( ) ) )
}
} ;
}
}
let session_token = match session ::create_session ( db , user_id , " " . to_string ( ) ) . await {
Ok ( s ) = > s ,
Err ( e ) = > {
return match e {
CreateSessionError ::UserDoesntExist = > {
LoginResponse ::Failure ( LoginError ::InvalidAccount )
}
CreateSessionError ::DatabaseError = > {
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while creating session " . to_string ( ) ) )
}
} ;
}
} ;
let death_date = match session ::get_session_death_date ( db , session_token . clone ( ) ) . await {
Ok ( s ) = > s ,
Err ( e ) = > {
return match e {
FetchSessionError ::SessionInvalid = > {
error! ( " unexpected session invalid after just creating it during login! " ) ;
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while creating session " . to_string ( ) ) )
}
FetchSessionError ::SessionDoesntExist = > {
error! ( " unexpected session not found after just creating it during login! " ) ;
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while creating session " . to_string ( ) ) )
}
FetchSessionError ::DatabaseError = > {
LoginResponse ::Failure ( LoginError ::InternalServer ( " database error while creating session " . to_string ( ) ) )
}
} ;
}
} ;
LoginResponse ::Success ( TokenAssignment {
token : session_token ,
ttl : death_date . timestamp ( ) ,
} )
}
pub async fn verify_session ( db : & DatabaseConnection , request : AuthRequest ) -> AuthResponse {
let uid = match session ::get_userid_from_session ( db , request . token , true ) . await {
Ok ( u ) = > u ,
Err ( e ) = > {
return match e {
FetchSessionError ::SessionInvalid = > {
AuthResponse ::Failure ( AuthError ::TokenExpired )
}
FetchSessionError ::SessionDoesntExist = > {
AuthResponse ::Failure ( AuthError ::TokenInvalid )
}
FetchSessionError ::DatabaseError = > {
AuthResponse ::Failure ( AuthError ::InternalServer ( " database error while querying user id " . to_string ( ) ) )
}
} ;
}
} ;
AuthResponse ::Success ( uid )
}
pub async fn logout ( db : & DatabaseConnection , request : LogoutRequest ) -> LogoutResponse {
match session ::delete_session ( db , request . token ) . await {
Ok ( _ ) = > LogoutResponse ::Success ,
Err ( e ) = > match e {
DeleteSessionError ::SessionDoesntExist = > { LogoutResponse ::BadTokenError }
DeleteSessionError ::DatabaseError = > { LogoutResponse ::InternalServerError ( " database error while deleting session " . to_string ( ) ) }
}
}
}
pub async fn email_change ( db : & DatabaseConnection , request : EmailChangeRequest ) -> EmailChangeResponse {
let uid = match session ::get_userid_from_session ( db , request . token , true ) . await {
Ok ( u ) = > u ,
Err ( e ) = > {
return match e {
FetchSessionError ::SessionInvalid = > {
EmailChangeResponse ::InvalidToken
}
FetchSessionError ::SessionDoesntExist = > {
EmailChangeResponse ::InvalidToken
}
FetchSessionError ::DatabaseError = > {
EmailChangeResponse ::InternalError
}
} ;
}
} ;
let verification_code = generate_email_verification_code ( ) ;
let res = user ::start_email_change ( db , uid . clone ( ) , request . new_email , verification_code . clone ( ) ) . await ;
if let Err ( e ) = res {
return match e {
EmailChangeError ::UserNotFound = > {
EmailChangeResponse ::InvalidToken
}
EmailChangeError ::NewEmailIsInvalid = > {
EmailChangeResponse ::InvalidEmail
}
EmailChangeError ::DatabaseError = > {
EmailChangeResponse ::InternalError
}
} ;
}
debug! ( " verification code for new email on {} is \" {} \" " , { user ::user_info ( db , uid . clone ( ) ) . await . map ( | v | v . username ) . unwrap_or ( uid . clone ( ) ) } , verification_code ) ;
EmailChangeResponse ::Success
}
pub async fn cancel_email_change ( db : & DatabaseConnection , request : CancelEmailChangeRequest ) -> CancelEmailChangeResponse {
let uid = match session ::get_userid_from_session ( db , request . token , true ) . await {
Ok ( u ) = > u ,
Err ( e ) = > {
return match e {
FetchSessionError ::SessionInvalid = > {
CancelEmailChangeResponse ::InvalidToken
}
FetchSessionError ::SessionDoesntExist = > {
CancelEmailChangeResponse ::InvalidToken
}
FetchSessionError ::DatabaseError = > {
CancelEmailChangeResponse ::InternalError
}
} ;
}
} ;
if let Err ( e ) = user ::cancel_email_change ( db , uid ) . await {
return match e {
FetchUserError ::UserNotFound = > {
CancelEmailChangeResponse ::InvalidToken
}
FetchUserError ::DatabaseError = > {
CancelEmailChangeResponse ::InternalError
}
} ;
}
CancelEmailChangeResponse ::Success
}