fix(saas): 3 项 P0 安全/功能修复 + TRUTH.md 数字校准
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0-01: Admin ApiKeys 创建功能前后端不匹配
- 前端 service 从 /keys 改回 /tokens(api_tokens 表)
- 前端 UI 字段 {name, expires_days, permissions} 与旧路由匹配
P0-02: 账户锁定检查错误处理
- unwrap_or(false) 改为 map_err + SaasError 传播
- SQL 查询失败时返回错误而非静默跳过锁定检查
P0-03: Logout refresh token 撤销增强
- 新增 access token cookie fallback 提取 account_id
- Tauri 桌面端 Bearer auth 场景下也能撤销 refresh token
TRUTH.md 校准: Tauri 183→190, invoke 95→104, .route() 136→137, 中间件 15→14
This commit is contained in:
@@ -215,7 +215,10 @@ pub async fn login(
|
||||
.bind(&r.id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
.map_err(|e| {
|
||||
tracing::warn!(account_id = %r.id, error = %e, "Lockout check query failed");
|
||||
SaasError::Internal("账号状态检查失败,请重试".into())
|
||||
})?;
|
||||
|
||||
if is_locked {
|
||||
return Err(SaasError::AuthError("账号已被临时锁定,请稍后再试".into()));
|
||||
@@ -631,5 +634,32 @@ pub async fn logout(
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: 如果没有找到 refresh token,尝试从 access token cookie 提取 account_id
|
||||
// Tauri 桌面端使用 Bearer auth 时,logout body 可能不含 refresh_token
|
||||
if tokens_to_check.is_empty() {
|
||||
if let Some(access_cookie) = jar.get(ACCESS_TOKEN_COOKIE) {
|
||||
let access_val = access_cookie.value().to_string();
|
||||
if let Ok(claims) = verify_token_skip_expiry(&access_val, jwt_secret) {
|
||||
let now = chrono::Utc::now();
|
||||
let result = sqlx::query(
|
||||
"UPDATE refresh_tokens SET used_at = $1 WHERE account_id = $2 AND used_at IS NULL"
|
||||
)
|
||||
.bind(&now)
|
||||
.bind(&claims.sub)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) => {
|
||||
tracing::info!(account_id = %claims.sub, n = r.rows_affected(), "Refresh tokens revoked via access token fallback");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(account_id = %claims.sub, error = %e, "Failed to revoke refresh tokens (access fallback)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(clear_auth_cookies(jar), axum::http::StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user