use crate::config::AuthConfig;
use crate::core::auth::{self, create_jwt};
use crate::core::key_service::{KeyService, KeyUploadParams};
use crate::core::user::{OneTimePreKey, SignedPreKey};
use crate::error::{AppError, Result};
use crate::storage::DbPool;
use crate::storage::refresh_token_repo::RefreshTokenRepository;
use crate::storage::user_repo::UserRepository;
use serde::Serialize;
use uuid::Uuid;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthResponse {
pub token: String,
pub refresh_token: String,
pub expires_at: i64,
}
#[derive(Clone)]
pub struct AccountService {
pool: DbPool,
config: AuthConfig,
key_service: KeyService,
user_repo: UserRepository,
refresh_repo: RefreshTokenRepository,
}
impl AccountService {
pub fn new(
pool: DbPool,
config: AuthConfig,
key_service: KeyService,
user_repo: UserRepository,
refresh_repo: RefreshTokenRepository,
) -> Self {
Self { pool, config, key_service, user_repo, refresh_repo }
}
pub async fn register(
&self,
username: String,
password: String,
identity_key: crate::core::crypto_types::PublicKey,
registration_id: i32,
signed_pre_key: SignedPreKey,
one_time_pre_keys: Vec<OneTimePreKey>,
) -> Result<AuthResponse> {
KeyService::validate_otpk_uniqueness(&one_time_pre_keys)?;
let password_hash: Result<String> = tokio::task::spawn_blocking(move || auth::hash_password(&password))
.await
.map_err(|_| AppError::Internal)?;
let password_hash = password_hash?;
let mut tx = self.pool.begin().await?;
let user = self.user_repo.create(&mut *tx, &username, &password_hash).await.map_err(|e| {
if let AppError::Database(sqlx::Error::Database(db_err)) = &e
&& db_err.code().as_deref() == Some("23505")
{
return AppError::Conflict("Username already exists".into());
}
e
})?;
let key_params = KeyUploadParams {
user_id: user.id,
identity_key: Some(identity_key),
registration_id: Some(registration_id),
signed_pre_key,
one_time_pre_keys,
};
self.key_service.upload_keys_internal(&mut tx, key_params).await?;
let token = create_jwt(user.id, &self.config.jwt_secret, self.config.access_token_ttl_secs)?;
let refresh_token = auth::generate_opaque_token();
let refresh_hash = auth::hash_token(&refresh_token);
self.refresh_repo.create(&mut *tx, user.id, &refresh_hash, self.config.refresh_token_ttl_days).await?;
tx.commit().await?;
let expires_at = (time::OffsetDateTime::now_utc()
+ time::Duration::seconds(self.config.access_token_ttl_secs as i64))
.unix_timestamp();
Ok(AuthResponse { token, refresh_token, expires_at })
}
pub async fn login(&self, username: String, password: String) -> Result<AuthResponse> {
let user = self.user_repo.find_by_username(&self.pool, &username).await?.ok_or(AppError::AuthError)?;
let password_hash = user.password_hash.clone();
let is_valid: Result<bool> =
tokio::task::spawn_blocking(move || auth::verify_password(&password, &password_hash))
.await
.map_err(|_| AppError::Internal)?;
let is_valid = is_valid?;
if !is_valid {
return Err(AppError::AuthError);
}
let token = create_jwt(user.id, &self.config.jwt_secret, self.config.access_token_ttl_secs)?;
let refresh_token = auth::generate_opaque_token();
let refresh_hash = auth::hash_token(&refresh_token);
let mut tx = self.pool.begin().await?;
self.refresh_repo.create(&mut *tx, user.id, &refresh_hash, self.config.refresh_token_ttl_days).await?;
tx.commit().await?;
let expires_at = (time::OffsetDateTime::now_utc()
+ time::Duration::seconds(self.config.access_token_ttl_secs as i64))
.unix_timestamp();
Ok(AuthResponse { token, refresh_token, expires_at })
}
pub async fn refresh(&self, refresh_token: String) -> Result<AuthResponse> {
let hash = auth::hash_token(&refresh_token);
let mut tx = self.pool.begin().await?;
let user_id = self.refresh_repo.verify_and_consume(&mut tx, &hash).await?.ok_or(AppError::AuthError)?;
let new_access_token = create_jwt(user_id, &self.config.jwt_secret, self.config.access_token_ttl_secs)?;
let new_refresh_token = auth::generate_opaque_token();
let new_refresh_hash = auth::hash_token(&new_refresh_token);
self.refresh_repo.create(&mut *tx, user_id, &new_refresh_hash, self.config.refresh_token_ttl_days).await?;
tx.commit().await?;
let expires_at = (time::OffsetDateTime::now_utc()
+ time::Duration::seconds(self.config.access_token_ttl_secs as i64))
.unix_timestamp();
Ok(AuthResponse { token: new_access_token, refresh_token: new_refresh_token, expires_at })
}
pub async fn logout(&self, user_id: Uuid, refresh_token: String) -> Result<()> {
let hash = auth::hash_token(&refresh_token);
self.refresh_repo.delete_owned(&self.pool, &hash, user_id).await?;
Ok(())
}
}