All checks were successful
/ build-all-services (push) Successful in 9m14s
Fixes #T116
349 lines
15 KiB
Rust
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
|
|
}
|