use crate::persistence::sql::{
signup_code::{SignupCode, SignupCodeRepository},
uexecutor, SqlDb,
};
use crate::services::user_service::{UserEntity, UserService};
use crate::shared::quota::UserQuota;
use crate::SignupMode;
use pubky_common::crypto::PublicKey;
#[derive(Debug, thiserror::Error)]
pub enum SignupServiceError {
#[error("User already exists")]
UserAlreadyExists,
#[error("Token required")]
SignupTokenRequired,
#[error("Invalid token")]
InvalidSignupToken,
#[error("Token already used")]
SignupTokenAlreadyUsed,
#[error("Internal error: {0}")]
Internal(#[from] sqlx::Error),
}
#[derive(Clone, Debug)]
pub struct SignupService {
sql_db: SqlDb,
signup_mode: SignupMode,
user_service: UserService,
}
impl SignupService {
pub(crate) fn from_context(context: &crate::AppContext) -> Self {
Self {
sql_db: context.sql_db.clone(),
signup_mode: context.config_toml.general.signup_mode.clone(),
user_service: context.user_service.clone(),
}
}
#[cfg(test)]
pub fn new(sql_db: SqlDb, signup_mode: SignupMode, user_service: UserService) -> Self {
Self {
sql_db,
signup_mode,
user_service,
}
}
pub async fn create_new_user(
&self,
public_key: &PublicKey,
signup_token: Option<&SignupCode>,
) -> Result<UserEntity, SignupServiceError> {
let mut tx = self.sql_db.pool().begin().await?;
let user = self
.create_user_in_tx(public_key, signup_token, &mut tx)
.await?;
tx.commit().await?;
self.user_service.cache_user_quota(&user);
Ok(user)
}
pub(crate) fn cache_user_quota(&self, user: &UserEntity) {
self.user_service.cache_user_quota(user);
}
pub(crate) async fn create_user_in_tx(
&self,
public_key: &PublicKey,
signup_token: Option<&SignupCode>,
tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
) -> Result<UserEntity, SignupServiceError> {
self.ensure_user_not_exists(public_key, tx).await?;
let quota = if self.signup_mode == SignupMode::TokenRequired {
Self::validate_and_consume_signup_token(signup_token, public_key, tx).await?
} else {
UserQuota::default()
};
let user = self
.user_service
.create_in_tx(public_key, uexecutor!(*tx))
.await?;
let user = self
.user_service
.set_quota_in_tx(user.id, "a, uexecutor!(*tx))
.await?;
Ok(user)
}
async fn ensure_user_not_exists(
&self,
public_key: &PublicKey,
tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
) -> Result<(), SignupServiceError> {
match self
.user_service
.get_in_tx(public_key, uexecutor!(*tx))
.await
{
Ok(_) => Err(SignupServiceError::UserAlreadyExists),
Err(sqlx::Error::RowNotFound) => Ok(()),
Err(e) => Err(e.into()),
}
}
async fn validate_and_consume_signup_token(
signup_token: Option<&SignupCode>,
public_key: &PublicKey,
tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
) -> Result<UserQuota, SignupServiceError> {
let code_id = signup_token.ok_or(SignupServiceError::SignupTokenRequired)?;
let code = match SignupCodeRepository::get(code_id, uexecutor!(*tx)).await {
Ok(code) => code,
Err(sqlx::Error::RowNotFound) => return Err(SignupServiceError::InvalidSignupToken),
Err(e) => return Err(SignupServiceError::Internal(e)),
};
if code.used_by.is_some() {
return Err(SignupServiceError::SignupTokenAlreadyUsed);
}
let quota = code.quota();
SignupCodeRepository::mark_as_used(code_id, public_key, uexecutor!(*tx)).await?;
Ok(quota)
}
}