use std::time::{Duration, SystemTime};
use super::issuance::issue_session_auth;
use crate::{
AccessToken, AuthError, Claims, DisplayName, Email, NewUser, NythosResult, Password,
PasswordHasher, RefreshToken, Session, SessionStore, TenantAuthPolicy, TenantId,
TenantPolicyPort, TokenSigner, User, UserRepository, Username,
};
#[derive(Debug, Clone)]
pub struct RegisterInput {
tenant_id: TenantId,
email: String,
password: String,
username: Option<String>,
display_name: Option<String>,
issued_at: SystemTime,
access_token_ttl: Duration,
session_ttl: Duration,
auto_sign_in: bool,
}
impl RegisterInput {
pub fn new(
tenant_id: TenantId,
email: String,
password: String,
issued_at: SystemTime,
access_token_ttl: Duration,
session_ttl: Duration,
) -> Self {
Self {
tenant_id,
email,
password,
username: None,
display_name: None,
issued_at,
access_token_ttl,
session_ttl,
auto_sign_in: true,
}
}
pub const fn tenant_id(&self) -> TenantId {
self.tenant_id
}
pub fn email(&self) -> &str {
&self.email
}
pub fn password(&self) -> &str {
&self.password
}
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
pub fn display_name(&self) -> Option<&str> {
self.display_name.as_deref()
}
pub const fn issued_at(&self) -> SystemTime {
self.issued_at
}
pub const fn access_token_ttl(&self) -> Duration {
self.access_token_ttl
}
pub const fn session_ttl(&self) -> Duration {
self.session_ttl
}
pub const fn auto_sign_in(&self) -> bool {
self.auto_sign_in
}
pub fn with_profile(mut self, username: Option<String>, display_name: Option<String>) -> Self {
self.username = username;
self.display_name = display_name;
self
}
pub fn with_username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = Some(display_name.into());
self
}
pub fn with_auto_sign_in(mut self, auto_sign_in: bool) -> Self {
self.auto_sign_in = auto_sign_in;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisterAuthMaterial {
user: User,
session: Session,
refresh_token: RefreshToken,
access_token: AccessToken,
claims: Claims,
}
impl RegisterAuthMaterial {
pub fn new(
user: User,
session: Session,
refresh_token: RefreshToken,
access_token: AccessToken,
claims: Claims,
) -> Self {
Self {
user,
session,
refresh_token,
access_token,
claims,
}
}
pub fn user(&self) -> &User {
&self.user
}
pub fn session(&self) -> &Session {
&self.session
}
pub fn refresh_token(&self) -> &RefreshToken {
&self.refresh_token
}
pub fn access_token(&self) -> &AccessToken {
&self.access_token
}
pub fn claims(&self) -> &Claims {
&self.claims
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisterResult {
user: User,
auth: Option<RegisterAuthMaterial>,
}
impl RegisterResult {
pub fn new(user: User, auth: Option<RegisterAuthMaterial>) -> Self {
Self { user, auth }
}
pub fn user(&self) -> &User {
&self.user
}
pub fn auth(&self) -> Option<&RegisterAuthMaterial> {
self.auth.as_ref()
}
}
pub struct RegisterService<'a, U, P, S, H, T> {
user_repository: &'a U,
tenant_policy_port: &'a P,
session_store: &'a S,
password_hasher: &'a H,
token_signer: &'a T,
}
impl<'a, U, P, S, H, T> RegisterService<'a, U, P, S, H, T>
where
U: UserRepository,
P: TenantPolicyPort,
S: SessionStore,
H: PasswordHasher,
T: TokenSigner,
{
pub fn new(
user_repository: &'a U,
tenant_policy_port: &'a P,
session_store: &'a S,
password_hasher: &'a H,
token_signer: &'a T,
) -> Self {
Self {
user_repository,
tenant_policy_port,
session_store,
password_hasher,
token_signer,
}
}
pub async fn register(&self, input: RegisterInput) -> NythosResult<RegisterResult> {
let email = Email::parse(input.email())?;
let password = Password::new(input.password())?;
let policy = self
.tenant_policy_port
.load_auth_policy(input.tenant_id())
.await?;
let username = self.parse_username(input.username(), &policy)?;
let display_name = self.parse_display_name(input.display_name(), &policy)?;
self.ensure_email_available(input.tenant_id(), &email)
.await?;
if let Some(username) = &username {
self.ensure_username_available(input.tenant_id(), username)
.await?;
}
let password_hash = self.password_hasher.hash(&password).await?;
let user = self
.user_repository
.create(
input.tenant_id(),
NewUser::with_profile(email, username, display_name),
password_hash,
)
.await?;
let auth = if input.auto_sign_in() {
Some(self.create_auth_material(&input, &user).await?)
} else {
None
};
Ok(RegisterResult::new(user, auth))
}
fn parse_username(
&self,
username: Option<&str>,
policy: &TenantAuthPolicy,
) -> NythosResult<Option<Username>> {
let Some(username) = username else {
return Ok(None);
};
if !policy.username_registration_enabled() {
return Err(AuthError::ValidationError(
"username registration is disabled for tenant".to_owned(),
));
}
Username::parse(username).map(Some)
}
fn parse_display_name(
&self,
display_name: Option<&str>,
policy: &TenantAuthPolicy,
) -> NythosResult<Option<DisplayName>> {
let Some(display_name) = display_name else {
return Ok(None);
};
if !policy.display_name_registration_enabled() {
return Err(AuthError::ValidationError(
"display name registration is disabled for tenant".to_owned(),
));
}
DisplayName::parse(display_name).map(Some)
}
async fn ensure_email_available(&self, tenant_id: TenantId, email: &Email) -> NythosResult<()> {
if self
.user_repository
.find_by_email(tenant_id, email)
.await?
.is_some()
{
return Err(AuthError::ValidationError(
"user with email already exists in tenant".to_owned(),
));
}
Ok(())
}
async fn ensure_username_available(
&self,
tenant_id: TenantId,
username: &Username,
) -> NythosResult<()> {
if self
.user_repository
.find_by_username(tenant_id, username)
.await?
.is_some()
{
return Err(AuthError::ValidationError(
"user with username already exists in tenant".to_owned(),
));
}
Ok(())
}
async fn create_auth_material(
&self,
input: &RegisterInput,
user: &User,
) -> NythosResult<RegisterAuthMaterial> {
let issued = issue_session_auth(
self.session_store,
self.token_signer,
user.id(),
input.tenant_id(),
input.issued_at(),
input.access_token_ttl(),
input.session_ttl(),
)
.await?;
Ok(RegisterAuthMaterial::new(
user.clone(),
issued.session,
issued.refresh_token,
issued.access_token,
issued.claims,
))
}
}