mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
feat: README, config.example, turnstile integration, exchange rate, auth utils refactor
This commit is contained in:
@@ -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("a_repos), Arc::clone("a_config))
|
||||
mux.handle_async_func(crate::queue::types::FORTHWITH_QUOTA_TASK, move |t| {
|
||||
task::quota_task(t, Arc::clone("a_repos), Arc::clone("a_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
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user