pub mod config;
pub mod error;
pub mod models;
pub mod client;
pub mod db;
#[cfg(feature = "axum-integration")]
pub mod axum;
use std::sync::Arc;
use rand::Rng;
use sqlx::PgPool;
pub use config::FutureAuthConfig;
pub use error::{Result, FutureAuthError};
pub use models::{OtpChannel, Session, User};
pub struct FutureAuth {
pub pool: PgPool,
pub config: FutureAuthConfig,
http: reqwest::Client,
}
impl FutureAuth {
pub fn new(pool: PgPool, config: FutureAuthConfig) -> Arc<Self> {
Arc::new(Self {
pool,
config,
http: reqwest::Client::new(),
})
}
pub async fn ensure_tables(&self) -> Result<()> {
db::migrations::ensure_tables(&self.pool).await
}
pub async fn send_otp(&self, channel: OtpChannel, destination: &str) -> Result<()> {
let code = generate_otp(self.config.otp_length);
db::verification::create(&self.pool, destination, &code, self.config.otp_ttl).await?;
client::send_otp(&self.http, &self.config, channel, destination, &code).await?;
tracing::info!("OTP sent to {destination} via {channel:?}");
Ok(())
}
pub async fn verify_otp(
&self,
identifier: &str,
code: &str,
ip_address: Option<&str>,
user_agent: Option<&str>,
) -> Result<(User, Session)> {
db::verification::verify(&self.pool, identifier, code).await?;
let user = if identifier.contains('@') {
db::user::find_or_create_by_email(&self.pool, identifier).await?
} else {
db::user::find_or_create_by_phone(&self.pool, identifier).await?
};
let session = db::session::create(
&self.pool,
&user.id,
self.config.session_ttl,
ip_address,
user_agent,
)
.await?;
Ok((user, session))
}
pub async fn get_session(&self, token: &str) -> Result<Option<(User, Session)>> {
db::session::find_by_token(&self.pool, token).await
}
pub async fn revoke_session(&self, token: &str) -> Result<()> {
db::session::revoke(&self.pool, token).await
}
pub async fn revoke_all_sessions(&self, user_id: &str) -> Result<()> {
db::session::revoke_all_for_user(&self.pool, user_id).await
}
pub async fn cleanup_expired(&self) -> Result<(u64, u64)> {
let sessions = db::session::cleanup_expired(&self.pool).await?;
let verifications = db::verification::cleanup_expired(&self.pool).await?;
Ok((sessions, verifications))
}
pub async fn get_user(&self, id: &str) -> Result<Option<User>> {
db::user::find_by_id(&self.pool, id).await
}
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
db::user::find_by_email(&self.pool, email).await
}
pub async fn get_user_by_phone(&self, phone: &str) -> Result<Option<User>> {
db::user::find_by_phone(&self.pool, phone).await
}
pub async fn update_user_name(&self, user_id: &str, name: &str) -> Result<User> {
db::user::update_name(&self.pool, user_id, name).await
}
pub async fn set_user_metadata(&self, user_id: &str, metadata: serde_json::Value) -> Result<User> {
db::user::set_metadata(&self.pool, user_id, metadata).await
}
pub async fn merge_user_metadata(&self, user_id: &str, patch: serde_json::Value) -> Result<User> {
db::user::merge_metadata(&self.pool, user_id, patch).await
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
}
fn generate_otp(length: usize) -> String {
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::thread_rng();
(0..length)
.map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
.collect()
}