use std::time::SystemTime;
use crate::{
AuthError, ExternalIdentity, ExternalIdentityRepository, NythosResult, OAuthProviderKind,
TenantId, TenantOAuthProviderConfigPort, User, UserId, UserRepository, VerifiedExternalProfile,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OAuthLoginOutcome {
ProviderDisabled { provider_kind: OAuthProviderKind },
ExistingIdentityLogin { user_id: UserId },
LinkRequired {
user_id: UserId,
profile: VerifiedExternalProfile,
},
RegistrationRequired {
profile: VerifiedExternalProfile,
registration_allowed: bool,
},
}
pub struct OAuthLoginService<'a, I, U, C>
where
I: ExternalIdentityRepository,
U: UserRepository,
C: TenantOAuthProviderConfigPort,
{
identity_repository: &'a I,
user_repository: &'a U,
oauth_config_port: &'a C,
}
impl<'a, I, U, C> OAuthLoginService<'a, I, U, C>
where
I: ExternalIdentityRepository,
U: UserRepository,
C: TenantOAuthProviderConfigPort,
{
pub fn new(
identity_repository: &'a I,
user_repository: &'a U,
oauth_config_port: &'a C,
) -> Self {
Self {
identity_repository,
user_repository,
oauth_config_port,
}
}
pub fn identity_repository(&self) -> &'a I {
self.identity_repository
}
pub fn user_repository(&self) -> &'a U {
self.user_repository
}
pub fn oauth_config_port(&self) -> &'a C {
self.oauth_config_port
}
pub async fn resolve_login(
&self,
tenant_id: TenantId,
profile: VerifiedExternalProfile,
now: SystemTime,
) -> NythosResult<OAuthLoginOutcome> {
let provider_kind = profile.provider_kind();
let Some(config) = self
.oauth_config_port
.load_provider_config(tenant_id, provider_kind)
.await?
else {
return Ok(OAuthLoginOutcome::ProviderDisabled { provider_kind });
};
if !config.is_enabled() {
return Ok(OAuthLoginOutcome::ProviderDisabled { provider_kind });
}
if let Some(identity) = self
.identity_repository
.find_by_provider(tenant_id, provider_kind, profile.provider_subject())
.await?
{
let user = self
.user_repository
.find_by_id(tenant_id, identity.user_id())
.await?
.ok_or(AuthError::UserNotFoundOrInactive)?;
Self::ensure_user_is_active(&user)?;
self.identity_repository
.touch(tenant_id, provider_kind, profile.provider_subject(), now)
.await?;
return Ok(OAuthLoginOutcome::ExistingIdentityLogin {
user_id: identity.user_id(),
});
}
if let Some(email) = profile.verified_email()
&& let Some(user) = self.user_repository.find_by_email(tenant_id, email).await?
{
Self::ensure_user_is_active(&user)?;
return Ok(OAuthLoginOutcome::LinkRequired {
user_id: user.id(),
profile,
});
}
Ok(OAuthLoginOutcome::RegistrationRequired {
profile,
registration_allowed: config.registration_allowed(),
})
}
pub async fn link_identity(
&self,
tenant_id: TenantId,
user_id: UserId,
profile: VerifiedExternalProfile,
now: SystemTime,
) -> NythosResult<ExternalIdentity> {
let user = self
.user_repository
.find_by_id(tenant_id, user_id)
.await?
.ok_or(AuthError::UserNotFoundOrInactive)?;
Self::ensure_user_is_active(&user)?;
if let Some(existing_identity) = self
.identity_repository
.find_by_provider(
tenant_id,
profile.provider_kind(),
profile.provider_subject(),
)
.await?
{
if existing_identity.user_id() == user_id {
return Err(AuthError::OAuthIdentityAlreadyLinkedToSelf);
}
return Err(AuthError::OAuthIdentityAlreadyLinked);
}
let identity = ExternalIdentity::new(
tenant_id,
user_id,
profile.provider_kind(),
profile.provider_subject(),
profile.email().cloned(),
profile.display_name().cloned(),
now,
)?;
self.identity_repository.link(identity.clone()).await?;
Ok(identity)
}
fn ensure_user_is_active(user: &User) -> NythosResult<()> {
if !user.can_authenticate() {
return Err(AuthError::UserNotFoundOrInactive);
}
Ok(())
}
}