use std::sync::Arc;
use chrono::Duration;
use torii_core::{
JwtSessionProvider, OpaqueSessionProvider, RepositoryProvider, SessionProvider,
repositories::{
PasswordRepositoryAdapter, SessionRepositoryAdapter, TokenRepositoryAdapter,
UserRepositoryAdapter,
},
services::{SessionService, UserService},
};
#[cfg(feature = "oauth")]
use torii_core::repositories::OAuthRepositoryAdapter;
#[cfg(feature = "passkey")]
use torii_core::repositories::PasskeyRepositoryAdapter;
#[cfg(feature = "password")]
use torii_core::services::PasswordService;
#[cfg(feature = "oauth")]
use torii_core::services::OAuthService;
#[cfg(feature = "passkey")]
use torii_core::services::PasskeyService;
#[cfg(feature = "magic-link")]
use torii_core::services::MagicLinkService;
#[cfg(any(feature = "password", feature = "magic-link"))]
pub use torii_core::services::PasswordResetService;
#[cfg(feature = "mailer")]
use torii_core::services::{MailerService, ToriiMailerService};
pub use torii_core::{
JwtAlgorithm, JwtClaims, JwtConfig, JwtMetadata, Session, SessionToken, User, UserId,
};
pub use torii_core::storage::{SecureToken, TokenPurpose};
#[cfg(feature = "mailer")]
pub use torii_mailer::{MailerConfig, TemplateContext};
#[cfg(feature = "password")]
pub struct PasswordAuth<'a, R: RepositoryProvider> {
torii: &'a Torii<R>,
}
#[cfg(feature = "magic-link")]
pub struct MagicLinkAuth<'a, R: RepositoryProvider> {
torii: &'a Torii<R>,
}
#[cfg(feature = "oauth")]
pub struct OAuthAuth<'a, R: RepositoryProvider> {
torii: &'a Torii<R>,
}
#[cfg(feature = "passkey")]
pub struct PasskeyAuth<'a, R: RepositoryProvider> {
torii: &'a Torii<R>,
}
#[cfg(feature = "sqlite")]
pub mod sqlite {
pub use torii_storage_sqlite::{SqliteRepositoryProvider, SqliteStorage};
}
#[cfg(feature = "postgres")]
pub mod postgres {
pub use torii_storage_postgres::PostgresStorage;
}
#[cfg(any(
feature = "seaorm-sqlite",
feature = "seaorm-postgres",
feature = "seaorm-mysql",
feature = "seaorm"
))]
pub mod seaorm {
pub use torii_storage_seaorm::{SeaORMStorage, repositories::SeaORMRepositoryProvider};
}
#[derive(Debug, thiserror::Error)]
pub enum ToriiError {
#[error("Auth error: {0}")]
AuthError(String),
#[error("Storage error: {0}")]
StorageError(String),
}
pub struct SessionConfig {
pub expires_in: Duration,
pub provider_type: SessionProviderType,
}
pub enum SessionProviderType {
Opaque,
Jwt(JwtConfig),
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
expires_in: Duration::days(30),
provider_type: SessionProviderType::Opaque,
}
}
}
impl SessionConfig {
pub fn with_jwt(mut self, jwt_config: JwtConfig) -> Self {
self.provider_type = SessionProviderType::Jwt(jwt_config);
self
}
pub fn expires_in(mut self, duration: Duration) -> Self {
self.expires_in = duration;
self
}
}
pub struct Torii<R: RepositoryProvider> {
repositories: Arc<R>,
user_service: Arc<UserService<UserRepositoryAdapter<R>>>,
session_service: Arc<SessionService<Box<dyn SessionProvider>>>,
session_provider: Arc<Box<dyn SessionProvider>>,
#[cfg(feature = "password")]
password_service: Arc<PasswordService<UserRepositoryAdapter<R>, PasswordRepositoryAdapter<R>>>,
#[cfg(feature = "oauth")]
#[allow(dead_code)] oauth_service: Arc<OAuthService<UserRepositoryAdapter<R>, OAuthRepositoryAdapter<R>>>,
#[cfg(feature = "passkey")]
#[allow(dead_code)] passkey_service: Arc<PasskeyService<UserRepositoryAdapter<R>, PasskeyRepositoryAdapter<R>>>,
#[cfg(feature = "magic-link")]
#[allow(dead_code)] magic_link_service: Arc<MagicLinkService<UserRepositoryAdapter<R>, TokenRepositoryAdapter<R>>>,
#[cfg(any(feature = "password", feature = "magic-link"))]
password_reset_service: Arc<
PasswordResetService<
UserRepositoryAdapter<R>,
PasswordRepositoryAdapter<R>,
TokenRepositoryAdapter<R>,
>,
>,
#[cfg(feature = "mailer")]
mailer_service: Option<Arc<ToriiMailerService>>,
session_config: SessionConfig,
}
impl<R: RepositoryProvider> Torii<R> {
#[cfg(feature = "password")]
pub fn password(&self) -> PasswordAuth<'_, R> {
PasswordAuth { torii: self }
}
#[cfg(feature = "magic-link")]
pub fn magic_link(&self) -> MagicLinkAuth<R> {
MagicLinkAuth { torii: self }
}
#[cfg(feature = "oauth")]
pub fn oauth(&self) -> OAuthAuth<R> {
OAuthAuth { torii: self }
}
#[cfg(feature = "passkey")]
pub fn passkey(&self) -> PasskeyAuth<R> {
PasskeyAuth { torii: self }
}
}
impl<R: RepositoryProvider> Torii<R> {
pub fn new(repositories: Arc<R>) -> Self {
let user_repo = Arc::new(UserRepositoryAdapter::new(repositories.clone()));
let session_repo = Arc::new(SessionRepositoryAdapter::new(repositories.clone()));
let user_service = Arc::new(UserService::new(user_repo.clone()));
let session_provider: Arc<Box<dyn SessionProvider>> =
Arc::new(Box::new(OpaqueSessionProvider::new(session_repo)));
let session_service = Arc::new(SessionService::new(session_provider.clone()));
Self {
repositories: repositories.clone(),
user_service,
session_service,
session_provider,
#[cfg(feature = "password")]
password_service: Arc::new(PasswordService::new(
user_repo.clone(),
Arc::new(PasswordRepositoryAdapter::new(repositories.clone())),
)),
#[cfg(feature = "oauth")]
oauth_service: Arc::new(OAuthService::new(
user_repo.clone(),
Arc::new(torii_core::repositories::OAuthRepositoryAdapter::new(
repositories.clone(),
)),
)),
#[cfg(feature = "passkey")]
passkey_service: Arc::new(PasskeyService::new(
user_repo.clone(),
Arc::new(torii_core::repositories::PasskeyRepositoryAdapter::new(
repositories.clone(),
)),
)),
#[cfg(feature = "magic-link")]
magic_link_service: Arc::new(MagicLinkService::new(
user_repo.clone(),
Arc::new(torii_core::repositories::TokenRepositoryAdapter::new(
repositories.clone(),
)),
)),
#[cfg(any(feature = "password", feature = "magic-link"))]
password_reset_service: Arc::new(PasswordResetService::new(
user_repo,
Arc::new(PasswordRepositoryAdapter::new(repositories.clone())),
Arc::new(torii_core::repositories::TokenRepositoryAdapter::new(
repositories.clone(),
)),
)),
#[cfg(feature = "mailer")]
mailer_service: None,
session_config: SessionConfig::default(),
}
}
pub fn with_session_config(mut self, config: SessionConfig) -> Self {
let session_provider: Arc<Box<dyn SessionProvider>> = match &config.provider_type {
SessionProviderType::Opaque => {
let session_repo =
Arc::new(SessionRepositoryAdapter::new(self.repositories.clone()));
Arc::new(Box::new(OpaqueSessionProvider::new(session_repo)))
}
SessionProviderType::Jwt(jwt_config) => {
Arc::new(Box::new(JwtSessionProvider::new(jwt_config.clone())))
}
};
self.session_provider = session_provider.clone();
self.session_service = Arc::new(SessionService::new(session_provider));
self.session_config = config;
self
}
pub fn with_jwt_sessions(self, jwt_config: JwtConfig) -> Self {
let config = SessionConfig::default().with_jwt(jwt_config);
self.with_session_config(config)
}
#[cfg(feature = "mailer")]
pub fn with_mailer(mut self, mailer_config: MailerConfig) -> Result<Self, ToriiError> {
let mailer = ToriiMailerService::new(mailer_config)
.map_err(|e| ToriiError::StorageError(format!("Failed to configure mailer: {e}")))?;
self.mailer_service = Some(Arc::new(mailer));
Ok(self)
}
#[cfg(feature = "mailer")]
pub fn with_mailer_from_env(mut self) -> Result<Self, ToriiError> {
let mailer = ToriiMailerService::from_env().map_err(|e| {
ToriiError::StorageError(format!("Failed to configure mailer from environment: {e}"))
})?;
self.mailer_service = Some(Arc::new(mailer));
Ok(self)
}
pub async fn migrate(&self) -> Result<(), ToriiError> {
self.repositories
.migrate()
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn health_check(&self) -> Result<(), ToriiError> {
self.repositories
.health_check()
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn get_user(&self, user_id: &UserId) -> Result<Option<User>, ToriiError> {
self.user_service
.get_user(user_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn create_session(
&self,
user_id: &UserId,
user_agent: Option<String>,
ip_address: Option<String>,
) -> Result<Session, ToriiError> {
self.session_service
.create_session(
user_id,
user_agent,
ip_address,
self.session_config.expires_in,
)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn get_session(&self, session_id: &SessionToken) -> Result<Session, ToriiError> {
self.session_service
.get_session(session_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))?
.ok_or(ToriiError::StorageError("Session not found".to_string()))
}
pub async fn delete_session(&self, session_id: &SessionToken) -> Result<(), ToriiError> {
self.session_service
.delete_session(session_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), ToriiError> {
self.session_service
.delete_user_sessions(user_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn set_user_email_verified(&self, user_id: &UserId) -> Result<(), ToriiError> {
self.user_service
.verify_email(user_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
pub async fn delete_user(&self, user_id: &UserId) -> Result<(), ToriiError> {
self.user_service
.delete_user(user_id)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))
}
}
#[cfg(feature = "password")]
impl<R: RepositoryProvider> PasswordAuth<'_, R> {
fn torii(&self) -> &Torii<R> {
self.torii
}
pub async fn register(&self, email: &str, password: &str) -> Result<User, ToriiError> {
self.register_with_name(email, password, None).await
}
pub async fn register_with_name(
&self,
email: &str,
password: &str,
name: Option<&str>,
) -> Result<User, ToriiError> {
let torii = self.torii();
let user = torii
.password_service
.register_user(email, password, name.map(|n| n.to_string()))
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?;
#[cfg(feature = "mailer")]
if let Some(mailer) = &torii.mailer_service {
let user_name = user.name.as_deref();
if let Err(e) = mailer.send_welcome_email(&user.email, user_name).await {
tracing::warn!("Failed to send welcome email: {}", e);
}
}
Ok(user)
}
pub async fn authenticate(
&self,
email: &str,
password: &str,
user_agent: Option<String>,
ip_address: Option<String>,
) -> Result<(User, Session), ToriiError> {
let torii = self.torii();
let user = torii
.password_service
.authenticate(email, password)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?;
let session = torii
.create_session(&user.id, user_agent, ip_address)
.await?;
Ok((user, session))
}
pub async fn change_password(
&self,
user_id: &UserId,
old_password: &str,
new_password: &str,
) -> Result<(), ToriiError> {
let torii = self.torii();
let user = torii
.get_user(user_id)
.await?
.ok_or_else(|| ToriiError::AuthError("User not found".to_string()))?;
torii
.password_service
.change_password(user_id, old_password, new_password)
.await
.map_err(|e| ToriiError::StorageError(e.to_string()))?;
torii.delete_sessions_for_user(user_id).await?;
#[cfg(feature = "mailer")]
if let Some(mailer) = &torii.mailer_service {
let user_name = user.name.as_deref();
if let Err(e) = mailer
.send_password_changed_email(&user.email, user_name)
.await
{
tracing::warn!("Failed to send password changed email: {}", e);
}
}
Ok(())
}
#[cfg(any(feature = "password", feature = "magic-link"))]
pub async fn reset_password_initiate(
&self,
email: &str,
reset_url_base: &str,
) -> Result<(), ToriiError> {
let torii = self.torii();
let result = torii
.password_reset_service
.request_password_reset(email)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?;
#[cfg(feature = "mailer")]
if let Some((user, token)) = result {
if let Some(mailer) = &torii.mailer_service {
let reset_link =
format!("{}?token={}", reset_url_base.trim_end_matches('/'), token);
if let Err(e) = mailer
.send_password_reset_email(&user.email, &reset_link, user.name.as_deref())
.await
{
tracing::warn!("Failed to send password reset email: {}", e);
}
}
}
Ok(())
}
#[cfg(any(feature = "password", feature = "magic-link"))]
pub async fn reset_password_initiate_with_expiration(
&self,
email: &str,
reset_url_base: &str,
expires_in: Duration,
) -> Result<(), ToriiError> {
let torii = self.torii();
let result = torii
.password_reset_service
.request_password_reset_with_expiration(email, expires_in)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?;
#[cfg(feature = "mailer")]
if let Some((user, token)) = result {
if let Some(mailer) = &torii.mailer_service {
let reset_link =
format!("{}?token={}", reset_url_base.trim_end_matches('/'), token);
if let Err(e) = mailer
.send_password_reset_email(&user.email, &reset_link, user.name.as_deref())
.await
{
tracing::warn!("Failed to send password reset email: {}", e);
}
}
}
Ok(())
}
#[cfg(any(feature = "password", feature = "magic-link"))]
pub async fn reset_password_verify_token(&self, token: &str) -> Result<bool, ToriiError> {
self.torii()
.password_reset_service
.verify_reset_token(token)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
#[cfg(any(feature = "password", feature = "magic-link"))]
pub async fn reset_password_complete(
&self,
token: &str,
new_password: &str,
) -> Result<User, ToriiError> {
let torii = self.torii();
let user = torii
.password_reset_service
.reset_password(token, new_password)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?;
torii.delete_sessions_for_user(&user.id).await?;
#[cfg(feature = "mailer")]
if let Some(mailer) = &torii.mailer_service {
let user_name = user.name.as_deref();
if let Err(e) = mailer
.send_password_changed_email(&user.email, user_name)
.await
{
tracing::warn!("Failed to send password changed email: {}", e);
}
}
Ok(user)
}
}
#[cfg(feature = "magic-link")]
impl<R: RepositoryProvider> MagicLinkAuth<'_, R> {
fn torii(&self) -> &Torii<R> {
self.torii
}
pub async fn generate_token(&self, email: &str) -> Result<SecureToken, ToriiError> {
let torii = self.torii();
torii
.magic_link_service
.generate_token(email)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn send_link(
&self,
email: &str,
magic_link_url_base: &str,
) -> Result<SecureToken, ToriiError> {
let token = self.generate_token(email).await?;
let torii = self.torii();
#[cfg(feature = "mailer")]
if let Some(mailer) = &torii.mailer_service {
let magic_link = format!(
"{}?token={}",
magic_link_url_base.trim_end_matches('/'),
token.token
);
let user = torii
.user_service
.get_user_by_email(email)
.await
.ok()
.flatten();
let user_name = user.as_ref().and_then(|u| u.name.as_deref());
if let Err(e) = mailer
.send_magic_link_email(email, &magic_link, user_name)
.await
{
tracing::warn!("Failed to send magic link email: {}", e);
}
}
Ok(token)
}
pub async fn authenticate(
&self,
token: &str,
user_agent: Option<String>,
ip_address: Option<String>,
) -> Result<(User, Session), ToriiError> {
let torii = self.torii();
let user = torii
.magic_link_service
.verify_token(token)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?
.ok_or_else(|| ToriiError::AuthError("Invalid magic token".to_string()))?;
let session = torii
.create_session(&user.id, user_agent, ip_address)
.await?;
Ok((user, session))
}
}
#[cfg(feature = "oauth")]
impl<R: RepositoryProvider> OAuthAuth<'_, R> {
fn torii(&self) -> &Torii<R> {
self.torii
}
pub async fn get_or_create_user(
&self,
provider: &str,
subject: &str,
email: &str,
name: Option<String>,
) -> Result<User, ToriiError> {
let torii = self.torii();
torii
.oauth_service
.get_or_create_user(provider, subject, email, name)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn link_account(
&self,
user_id: &UserId,
provider: &str,
subject: &str,
) -> Result<(), ToriiError> {
let torii = self.torii();
torii
.oauth_service
.link_account(user_id, provider, subject)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn get_account(
&self,
provider: &str,
subject: &str,
) -> Result<Option<torii_core::OAuthAccount>, ToriiError> {
let torii = self.torii();
torii
.oauth_service
.get_account(provider, subject)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn store_pkce_verifier(
&self,
csrf_state: &str,
pkce_verifier: &str,
expires_in: chrono::Duration,
) -> Result<(), ToriiError> {
let torii = self.torii();
torii
.oauth_service
.store_pkce_verifier(csrf_state, pkce_verifier, expires_in)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn get_pkce_verifier(&self, csrf_state: &str) -> Result<Option<String>, ToriiError> {
let torii = self.torii();
torii
.oauth_service
.get_pkce_verifier(csrf_state)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn authenticate(
&self,
provider: &str,
subject: &str,
email: &str,
name: Option<String>,
user_agent: Option<String>,
ip_address: Option<String>,
) -> Result<(User, Session), ToriiError> {
let user = self
.get_or_create_user(provider, subject, email, name)
.await?;
let torii = self.torii();
let session = torii
.create_session(&user.id, user_agent, ip_address)
.await?;
Ok((user, session))
}
}
#[cfg(feature = "passkey")]
impl<R: RepositoryProvider> PasskeyAuth<'_, R> {
fn torii(&self) -> &Torii<R> {
self.torii
}
pub async fn register_credential(
&self,
user_id: &UserId,
credential_id: Vec<u8>,
public_key: Vec<u8>,
name: Option<String>,
) -> Result<torii_core::repositories::PasskeyCredential, ToriiError> {
let torii = self.torii();
torii
.passkey_service
.register_credential(user_id, credential_id, public_key, name)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn get_user_credentials(
&self,
user_id: &UserId,
) -> Result<Vec<torii_core::repositories::PasskeyCredential>, ToriiError> {
let torii = self.torii();
torii
.passkey_service
.get_user_credentials(user_id)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn get_credential(
&self,
credential_id: &[u8],
) -> Result<Option<torii_core::repositories::PasskeyCredential>, ToriiError> {
let torii = self.torii();
torii
.passkey_service
.get_credential(credential_id)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn authenticate(
&self,
credential_id: &[u8],
user_agent: Option<String>,
ip_address: Option<String>,
) -> Result<(User, Session), ToriiError> {
let torii = self.torii();
let user = torii
.passkey_service
.authenticate_credential(credential_id)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))?
.ok_or_else(|| ToriiError::AuthError("Invalid passkey credential".to_string()))?;
let session = torii
.create_session(&user.id, user_agent, ip_address)
.await?;
Ok((user, session))
}
pub async fn delete_credential(&self, credential_id: &[u8]) -> Result<(), ToriiError> {
let torii = self.torii();
torii
.passkey_service
.delete_credential(credential_id)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
pub async fn delete_user_credentials(&self, user_id: &UserId) -> Result<(), ToriiError> {
let torii = self.torii();
torii
.passkey_service
.delete_user_credentials(user_id)
.await
.map_err(|e| ToriiError::AuthError(e.to_string()))
}
}