feat(auth): 添加 Refresh Token 双 Token 机制

- 添加 /api/refresh-token 接口支持 Token 续期
- 登录接口返回 access_token 和 refresh_token
- 新增 refresh_tokens 表存储 refresh_token
- 部署脚本添加数据库备份和迁移功能
- deploy.sh 添加 4 项 API 测试
- 更新 AGENTS.md 文档
This commit is contained in:
2026-04-19 16:01:41 +08:00
parent 52991dcfd5
commit e14c85436b
11 changed files with 872 additions and 98 deletions

View File

@@ -1,4 +1,4 @@
use sqlx::{PgPool, postgres::PgPoolOptions};
use sqlx::{PgPool, postgres::PgPoolOptions, FromRow};
use std::env;
use std::error::Error;
use chrono::Utc;
@@ -476,3 +476,89 @@ pub async fn set_weather_favorite(
Ok(())
}
// ===== Refresh Token DB 函数 =====
#[derive(Debug, FromRow)]
pub struct RefreshToken {
pub id: i32,
pub user_id: i32,
pub token: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn create_refresh_token(
pool: &PgPool,
user_id: i32,
token: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<(), AppError> {
let query = r#"
INSERT INTO refresh_tokens (user_id, token, expires_at)
VALUES ($1, $2, $3)
"#;
sqlx::query(query)
.bind(user_id)
.bind(token)
.bind(expires_at)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("创建 refresh_token 失败: {}", e)))?;
Ok(())
}
pub async fn verify_refresh_token(
pool: &PgPool,
token: &str,
) -> Result<RefreshToken, AppError> {
let query = r#"
SELECT id, user_id, token, expires_at, created_at
FROM refresh_tokens
WHERE token = $1 AND expires_at > NOW()
"#;
sqlx::query_as::<_, RefreshToken>(query)
.bind(token)
.fetch_optional(pool)
.await
.map_err(|e| AppError::Database(format!("验证 refresh_token 失败: {}", e)))?
.ok_or_else(|| AppError::Unauthorized("Refresh token 无效或已过期".to_string()))
}
pub async fn delete_refresh_token(pool: &PgPool, user_id: i32) -> Result<(), AppError> {
let query = r#"DELETE FROM refresh_tokens WHERE user_id = $1"#;
sqlx::query(query)
.bind(user_id)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("删除 refresh_token 失败: {}", e)))?;
Ok(())
}
pub async fn delete_specific_refresh_token(pool: &PgPool, token: &str) -> Result<(), AppError> {
let query = r#"DELETE FROM refresh_tokens WHERE token = $1"#;
sqlx::query(query)
.bind(token)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("删除 refresh_token 失败: {}", e)))?;
Ok(())
}
pub async fn cleanup_expired_refresh_tokens(pool: &PgPool) -> Result<u64, AppError> {
let query = r#"DELETE FROM refresh_tokens WHERE expires_at <= NOW()"#;
let result = sqlx::query(query)
.execute(pool)
.await
.map_err(|e| AppError::Database(format!("清理过期 refresh_token 失败: {}", e)))?;
Ok(result.rows_affected())
}