asklyphe/authservice/src/process/basic_accounts.rs
Evie Viau 39dfa6816e
All checks were successful
/ build-all-services (push) Successful in 9m14s
feature: Implement AUTH_URL for authservice emails
Fixes #T116
2025-03-20 16:59:31 -07:00

349 lines
15 KiB
Rust

/*
* 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};
use crate::{email, AUTH_URL};
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);
email::send_verification_code_email(&request.email, &request.username, format!("{}/verify?username={}&token={}", AUTH_URL.as_str(), request.username, verification_code).as_str());
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
}