feat(service): expand service layer with new domain operations

- Add IM service modules: audit, channel roles, custom emojis, forum
  tags, integrations, invitations, repo links, slash commands, stages,
  voice, webhooks
- Add PR service modules: review requests, templates
- Add repo service modules: contributors, release assets, git extras
  (archive, branch rename, commit extras, diff/merge, tag, tree)
- Add user service: social (follow/block)
- Add internal auth service
- Update existing service modules with expanded functionality
- Remove deleted IM modules: articles, delivery trace, drafts,
  follows, messages, polls, presence, reactions, threads
This commit is contained in:
zhenyi
2026-06-10 18:49:32 +08:00
parent cec6dce955
commit 420dedbc1e
100 changed files with 3797 additions and 3839 deletions
+24 -2
View File
@@ -35,6 +35,7 @@ struct PendingEmailChange {
impl AuthService {
const EMAIL_CHANGE_PREFIX: &'static str = "auth:email_change:";
const EMAIL_CHANGE_TTL_SECS: u64 = 60 * 60;
const EMAIL_CHANGE_COOLDOWN_SECS: u64 = 60;
pub async fn auth_get_email(&self, ctx: &Session) -> Result<EmailResponse, AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
@@ -63,11 +64,20 @@ impl AuthService {
if new_email.is_empty() {
return Err(AppError::BadRequest("email is required".into()));
}
// Rate limiting: check cooldown
let cooldown_key = format!("{}cooldown:{}", Self::EMAIL_CHANGE_PREFIX, user_uid);
if self.ctx.cache.exists(&cooldown_key).await {
return Err(AppError::BadRequest(
"email change request was sent recently; please try again later".into(),
));
}
let password = self.auth_rsa_decode(ctx, params.password).await?;
let row = sqlx::query("SELECT password_hash FROM user_password WHERE user_id = $1")
.bind(user_uid)
.fetch_optional(self.ctx.db.reader())
.fetch_optional(self.ctx.db.writer())
.await
.map_err(AppError::Database)?
.ok_or(AppError::UserNotFound)?;
@@ -103,6 +113,7 @@ impl AuthService {
},
Some(Duration::from_secs(Self::EMAIL_CHANGE_TTL_SECS)),
)
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let domain = self.ctx.config.main_domain()?;
@@ -133,6 +144,16 @@ impl AuthService {
AppError::InternalServerError(e.to_string())
})?;
// Set cooldown after successful email send
let cooldown_key = format!("{}cooldown:{}", Self::EMAIL_CHANGE_PREFIX, user_uid);
if let Err(e) = self.ctx.cache.set(
&cooldown_key,
&true,
Some(Duration::from_secs(Self::EMAIL_CHANGE_COOLDOWN_SECS)),
).await {
tracing::warn!(error = %e, "Failed to set email change cooldown");
}
tracing::info!(new_email = %new_email, user_uid = %user_uid, "Email change verification sent");
Ok(())
}
@@ -148,6 +169,7 @@ impl AuthService {
self.ctx
.cache
.get::<PendingEmailChange>(&cache_key)
.await
.ok_or(AppError::NotFound(
"invalid or expired email verification token".into(),
))?;
@@ -195,7 +217,7 @@ impl AuthService {
txn.commit().await.map_err(|_| AppError::TxnError)?;
let _ = self.ctx.cache.delete(&cache_key);
let _ = self.ctx.cache.delete(&cache_key).await;
tracing::info!(new_email = %pending.new_email, user_uid = %pending.user_uid, "Email changed");
Ok(())
}