/* * 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 . */ 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 }