use std::future::Future;
use std::net::IpAddr;
use std::pin::Pin;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::events::{LoggedIn, LoginFailed, announce};
use super::{User, fingerprint, login};
use crate::db::Model;
use crate::{AppState, Result, Session};
const PENDING_FOR: i64 = 10 * 60;
const PENDING: &str = "_auth_pending";
pub(crate) type RequiredFn =
Arc<dyn Fn(User, AppState) -> Pin<Box<dyn Future<Output = Result<bool>> + Send>> + Send + Sync>;
#[derive(Clone)]
pub(crate) struct SecondFactor {
pub(crate) challenge: String,
pub(crate) required: RequiredFn,
}
pub(crate) fn second_factor<F, Fut>(challenge: &str, required: F) -> SecondFactor
where
F: Fn(User, AppState) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<bool>> + Send + 'static,
{
SecondFactor {
challenge: challenge.to_owned(),
required: Arc::new(move |user, state| Box::pin(required(user, state))),
}
}
#[derive(Serialize, Deserialize)]
struct Stored {
user_id: i64,
email: String,
remember: bool,
hash: String,
at: i64,
to: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PendingLogin {
pub user_id: i64,
pub email: String,
pub remember: bool,
hash: String,
to: String,
}
impl PendingLogin {
pub async fn locked_out(&self, state: &AppState, ip: Option<IpAddr>) -> Option<u64> {
state.throttle.blocked_for(&self.email, ip).await
}
pub async fn failed(&self, state: &AppState, ip: Option<IpAddr>) {
state.throttle.fail(&self.email, ip).await;
let event = LoginFailed {
email: self.email.clone(),
ip: ip.map(|ip| ip.to_string()),
};
announce(state, event).await;
}
}
pub(crate) fn begin(
session: &Session,
user: &User,
email: &str,
remember: bool,
to: String,
) -> Result {
let stored = Stored {
user_id: user.id,
email: email.to_owned(),
remember,
hash: fingerprint(&user.password),
at: crate::clock::unix_secs(),
to,
};
session.put(PENDING, stored)
}
pub(crate) fn clear(session: &Session) {
session.remove(PENDING);
}
pub fn pending_login(session: &Session) -> Option<PendingLogin> {
let stored: Stored = session.get(PENDING)?;
if crate::clock::unix_secs() - stored.at > PENDING_FOR {
session.remove(PENDING);
return None;
}
Some(PendingLogin {
user_id: stored.user_id,
email: stored.email,
remember: stored.remember,
hash: stored.hash,
to: stored.to,
})
}
pub async fn complete_login(
state: &AppState,
session: &Session,
pending: &PendingLogin,
ip: Option<IpAddr>,
) -> Result<Option<String>> {
session.remove(PENDING);
let Some(user) = User::find(&state.db, pending.user_id).await? else {
return Ok(None);
};
if fingerprint(&user.password) != pending.hash {
return Ok(None);
}
state.throttle.clear(&pending.email, ip).await;
let remember = pending.remember.then_some(state.config.remember_lifetime);
login(session, &user, remember)?;
super::account::mark_confirmed(session)?;
let event = LoggedIn {
user_id: user.id,
ip: ip.map(|ip| ip.to_string()),
};
announce(state, event).await;
Ok(Some(pending.to.clone()))
}