feat: README, config.example, turnstile integration, exchange rate, auth utils refactor

This commit is contained in:
Ember Moth
2026-07-05 21:15:17 +08:00
parent 25d190b90e
commit 4ffdc89421
19 changed files with 763 additions and 4 deletions
+9 -3
View File
@@ -77,11 +77,17 @@ pub fn register_all(repos: Arc<Repositories>, config: Arc<Config>) -> ServeMux {
},
);
// ── Quota task ────────────────────────────────────────────────────────────
// ── Quota task + Rate task ─────────────────────────────────────────────────
let quota_repos = Arc::clone(&repos);
let quota_config = Arc::clone(&config);
mux.handle_async_func(crate::queue::types::FORTHWITH_QUOTA_TASK, move |task| {
task::quota_task(task, Arc::clone(&quota_repos), Arc::clone(&quota_config))
mux.handle_async_func(crate::queue::types::FORTHWITH_QUOTA_TASK, move |t| {
task::quota_task(t, Arc::clone(&quota_repos), Arc::clone(&quota_config))
});
let rate_repos = Arc::clone(&repos);
let rate_config = Arc::clone(&config);
mux.handle_async_func(crate::queue::types::FORTHWITH_RATE_TASK, move |t| {
task::rate_task(t, Arc::clone(&rate_repos), Arc::clone(&rate_config))
});
mux
+10 -1
View File
@@ -4,7 +4,7 @@ use asynq::error::Result;
use asynq::task::Task;
use crate::config::Config;
use crate::queue::service::task::QuotaTaskLogic;
use crate::queue::service::task::{QuotaTaskLogic, RateLogic};
use crate::repository::Repositories;
pub async fn quota_task(task: Task, repos: Arc<Repositories>, config: Arc<Config>) -> Result<()> {
@@ -13,3 +13,12 @@ pub async fn quota_task(task: Task, repos: Arc<Repositories>, config: Arc<Config
.await
.map_err(|e| asynq::error::Error::other(e.to_string()))
}
/// Fetch the current exchange rate and persist it.
/// Scheduled daily at 01:00 by the scheduler (mirrors `rateLogic.go`).
pub async fn rate_task(_task: Task, repos: Arc<Repositories>, config: Arc<Config>) -> Result<()> {
RateLogic::new(repos, config)
.execute()
.await
.map_err(|e| asynq::error::Error::other(e.to_string()))
}
+54
View File
@@ -250,3 +250,57 @@ impl QuotaTaskLogic {
Ok(())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RateLogic — port of `server/queue/logic/task/rateLogic.go`
// Fetches the current exchange rate for the site currency and caches it.
// ─────────────────────────────────────────────────────────────────────────────
pub struct RateLogic {
repos: Arc<Repositories>,
config: Arc<Config>,
}
impl RateLogic {
pub fn new(repos: Arc<Repositories>, config: Arc<Config>) -> Self {
Self { repos, config }
}
/// Fetch exchange rate from apilayer.com and cache it in Redis.
///
/// Skips silently when `Currency.AccessKey` is not configured.
pub async fn execute(&self) -> anyhow::Result<()> {
let access_key = &self.config.currency.access_key;
if access_key.is_empty() {
tracing::debug!("[RateLogic] skip: no Currency.AccessKey configured");
return Ok(());
}
let from = &self.config.currency.unit;
if from.is_empty() || from.to_uppercase() == "CNY" {
// Already in base currency — rate is 1.0, nothing to fetch.
tracing::debug!("[RateLogic] skip: currency unit is CNY or empty");
return Ok(());
}
match crate::exchange_rate::convert(from, "CNY", 1.0, access_key).await {
Ok(rate) => {
tracing::info!(
"[RateLogic] exchange rate {from}→CNY = {rate:.6}"
);
// Persist rate to system config table.
let _ = self
.repos
.system
.update_value_by_category_key("currency", "exchange_rate", &rate.to_string())
.await
.map_err(|e| tracing::warn!("[RateLogic] persist rate failed: {e}"));
}
Err(e) => {
tracing::error!("[RateLogic] fetch exchange rate failed: {e}");
return Err(e);
}
}
Ok(())
}
}
+2
View File
@@ -28,3 +28,5 @@ pub const FORTHWITH_TRAFFIC_STATISTICS: &str = "forthwith:traffic:statistics";
// ── task / quota ───────────────────────────────────────────────────────
pub const FORTHWITH_QUOTA_TASK: &str = "forthwith:quota:task";
/// Daily exchange-rate refresh (mirrors Go `ForthwithRateTask`).
pub const FORTHWITH_RATE_TASK: &str = "forthwith:rate:task";
+1
View File
@@ -10,3 +10,4 @@ pub mod telephone_user_register_service;
pub mod trial_cache;
pub mod user_login_service;
pub mod user_register_service;
pub mod utils;
@@ -32,6 +32,14 @@ impl ResetPasswordService {
&self,
req: ResetPasswordRequest,
) -> Result<LoginResponse, anyhow::Error> {
// Turnstile (mirrors Go: Verify.ResetPasswordVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.reset_password_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
let cache_key = format!("{}:2:{}", AUTH_CODE_CACHE_KEY, req.email);
let cached = self
.cache
@@ -25,6 +25,14 @@ impl TelephoneLoginService {
}
pub async fn login(&self, req: TelephoneLoginRequest) -> Result<LoginResponse, anyhow::Error> {
// Turnstile (mirrors Go: Verify.LoginVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.login_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
let phone = format!("+{}{}", req.telephone_area_code, req.telephone);
let auth_method = self.repos.user.find_auth_method_by_open_id("mobile", &phone).await
@@ -25,6 +25,14 @@ impl TelephoneResetPasswordService {
}
pub async fn reset(&self, req: TelephoneResetPasswordRequest) -> Result<LoginResponse, anyhow::Error> {
// Turnstile (mirrors Go: Verify.ResetPasswordVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.reset_password_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
let phone = format!("+{}{}", req.telephone_area_code, req.telephone);
let cache_key = format!("{}:{}", crate::config::cache_key::AUTH_CODE_TELEPHONE_CACHE_KEY, phone);
@@ -26,6 +26,14 @@ impl TelephoneUserRegisterService {
}
pub async fn register(&self, req: TelephoneRegisterRequest) -> Result<LoginResponse, anyhow::Error> {
// Turnstile (mirrors Go: Verify.RegisterVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.register_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
if self.config.register.stop_register {
return Err(anyhow!(CodeError::new_err_code(error_code::STOP_REGISTER)));
}
+8
View File
@@ -24,6 +24,14 @@ impl UserLoginService {
}
pub async fn login(&self, req: UserLoginRequest) -> Result<LoginResponse, anyhow::Error> {
// Turnstile verification (mirrors Go: Verify.LoginVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.login_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
let user = self
.repos.user.find_one_by_email(&req.email).await
.map_err(|_| anyhow!(CodeError::new_err_code(error_code::DATABASE_QUERY_ERROR)))?
@@ -26,6 +26,14 @@ impl UserRegisterService {
}
pub async fn register(&self, req: UserRegisterRequest) -> Result<LoginResponse, anyhow::Error> {
// Turnstile (mirrors Go: Verify.RegisterVerify && !Debug)
super::utils::check_turnstile(
self.config.verify.register_verify && self.config.model != "dev",
&self.config.verify.turnstile_secret,
&req.cf_token,
&req.ip,
).await?;
let cfg = &self.config.register;
if cfg.stop_register {
+25
View File
@@ -0,0 +1,25 @@
//! Shared helpers for auth services.
use anyhow::anyhow;
use result::code_error::CodeError;
use result::error_code;
/// Verify a Cloudflare Turnstile token.
///
/// Returns `Err(TOO_MANY_REQUESTS)` when verification fails.
/// Call-site guards the `debug` flag — pass `enabled = config.verify.login_verify && config.model != "dev"`.
pub async fn check_turnstile(
enabled: bool,
secret: &str,
token: &str,
ip: &str,
) -> anyhow::Result<()> {
if !enabled {
return Ok(());
}
let ok = turnstile::verify(secret, token, ip).await.unwrap_or(false);
if !ok {
return Err(anyhow!(CodeError::new_err_code(error_code::TOO_MANY_REQUESTS)));
}
Ok(())
}