#[cfg(feature = "diesel")]
use diesel::{prelude::*, sql_types::*};
use actix_web::HttpRequest;
use crate::{
app::{App, AppTypes},
errors::Error,
hashing,
secret::{PasswordHash, Secret},
sessions,
tokens,
users::{UserID, UserState},
};
#[derive(Debug)]
pub enum Notification {
UserRegistered {temporary_password: Secret},
PasswordChanged,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Challenge<A: AppTypes> {
LogIn,
ResetPassword,
VerifyNewUser,
Custom(A::CustomChallenge),
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum NoCustomChallenges {}
impl NoCustomChallenges {
#[inline(always)]
pub fn never_happens(self) -> ! {
match self {}
}
}
#[cfg_attr(feature = "diesel", derive(QueryableByName))]
pub struct ChallengeData<A: AppTypes> {
#[cfg_attr(feature = "diesel", diesel(embed))]
pub user: A::User,
#[cfg_attr(feature = "diesel", diesel(embed))]
pub user_state: UserState,
#[cfg_attr(feature = "diesel", diesel(sql_type = Text))]
pub challenge: String,
#[cfg_attr(feature = "diesel", diesel(deserialize_as = String), diesel(sql_type = Text))]
pub code_hash: Secret,
#[cfg_attr(feature = "diesel", diesel(sql_type = Timestamp))]
pub expires: A::DateTime,
}
pub struct ChallengeOutcome<A: AppTypes> {
pub user: A::User,
pub user_state: UserState,
pub challenge: Challenge<A>,
}
pub async fn issue_login_challenge<A: App>(
app: &mut A,
user: &A::User,
) -> Result<(), A::Error> {
issue_challenge(app, user, Challenge::LogIn)
.await
}
#[derive(Debug, Clone, Copy)]
pub enum ReissueEmailVerificationOutcome {
Sent,
AlreadyVerified,
NoSuchUser,
}
pub async fn reissue_email_verification_challenge<A: App>(
app: &mut A,
user_identifier: &str,
) -> Result<ReissueEmailVerificationOutcome, A::Error> {
let user = app.get_user_data_by_identifier(user_identifier)
.await
.map_err(Into::into)?;
let Some(user) = user else {
return Ok(ReissueEmailVerificationOutcome::NoSuchUser);
};
if !user.state.require_email_verification {
return Ok(ReissueEmailVerificationOutcome::AlreadyVerified);
}
issue_challenge(app, &user.user, Challenge::VerifyNewUser)
.await?;
Ok(ReissueEmailVerificationOutcome::Sent)
}
pub async fn issue_custom_challenge<A: App>(
app: &mut A,
user: &A::User,
challenge: A::CustomChallenge,
) -> Result<(), A::Error> {
issue_challenge(app, user, Challenge::Custom(challenge))
.await
}
pub(crate) async fn issue_challenge<A: App>(
app: &mut A,
user: &A::User,
challenge: Challenge<A>,
) -> Result<(), A::Error> {
let (bare_code, code_hash) = hashing::generate_challenge_code_and_hash();
let expires_secs = 3600 * app.challenge_expire_after_hours() as u64;
let expires = app.time_now() + std::time::Duration::from_secs(expires_secs);
let challenge_str = to_json(&challenge)?;
let challenge_id = app
.insert_challenge(user, &challenge_str, code_hash, expires)
.await
.map_err(Into::into)?;
let code = tokens::pack(challenge_id, bare_code);
let result = app.send_challenge(user, challenge, code)
.await;
if let Err(e) = result {
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
return Err(e.into());
}
Ok(())
}
pub async fn complete_challenge<A: App>(
app: &mut A,
code: Secret,
request: &HttpRequest,
) -> Result<ChallengeOutcome<A>, A::Error> {
let (challenge_id, challenge_code) = tokens::unpack(code)
.ok_or(Error::IncorrectChallengeCode)?;
let data = app
.get_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?
.ok_or(Error::IncorrectChallengeCode)?;
if app.time_now() >= data.expires {
log::debug!("Challenge {challenge_id} has expired; deleting");
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
return Error::IncorrectChallengeCode.as_app_err();
}
let challenge = parse_json::<A>(data.challenge)?;
if !hashing::check_fast_hash(&challenge_code, &data.code_hash) {
log::info!("Invalid code for challenge {challenge_id}",);
return Error::IncorrectChallengeCode.as_app_err();
}
let user = data.user;
let mut user_state = data.user_state;
match challenge {
Challenge::ResetPassword => {
log::info!("Successful password reset challenge");
app.update_password(&user, PasswordHash::NONE, true)
.await
.map_err(Into::into)?;
user_state.has_password = false;
user_state.require_password_change = true;
},
Challenge::VerifyNewUser => {
log::info!("Successful email verification challenge");
},
Challenge::LogIn => {
log::info!("Successful email login challenge for user {}", user.id());
},
Challenge::Custom {..} => {
log::info!("Successful custom challenge");
},
};
if data.user_state.require_email_verification {
app.verify_user(&user)
.await
.map_err(Into::into)?;
log::info!("Email verified for user {}", user.id());
user_state.require_email_verification = false;
}
sessions::on_successful_challenge(app, &user, request)
.await?;
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
Ok(ChallengeOutcome {user, user_state, challenge})
}
fn parse_json<A: App>(value: impl AsRef<str>) -> Result<Challenge<A>, Error> {
serde_json::from_str(value.as_ref())
.map_err(Error::Serde)
}
fn to_json<A: App>(challenge: &Challenge<A>) -> Result<String, Error> {
serde_json::to_string(challenge)
.map_err(Error::Serde)
}