update
This commit is contained in:
@@ -8,6 +8,7 @@ mod m20260211_140550_player_matchups;
|
||||
mod m20260211_140551_messages;
|
||||
mod m20260211_140552_map_comments;
|
||||
mod m20260211_140553_tournament_settings;
|
||||
mod m20260213_094956_add_refresh_tokens;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -23,6 +24,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260211_140551_messages::Migration),
|
||||
Box::new(m20260211_140552_map_comments::Migration),
|
||||
Box::new(m20260211_140553_tournament_settings::Migration),
|
||||
Box::new(m20260213_094956_add_refresh_tokens::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
42
migration/src/m20260213_094956_add_refresh_tokens.rs
Normal file
42
migration/src/m20260213_094956_add_refresh_tokens.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(User::Table)
|
||||
.add_column(ColumnDef::new(User::OsuRefreshToken).string().null())
|
||||
.add_column(ColumnDef::new(User::GuRefreshToken).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(User::Table)
|
||||
.drop_column(User::OsuRefreshToken)
|
||||
.drop_column(User::GuRefreshToken)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum User {
|
||||
Table,
|
||||
OsuRefreshToken,
|
||||
GuRefreshToken,
|
||||
}
|
||||
212
src/api/auth.rs
212
src/api/auth.rs
@@ -1,3 +1,4 @@
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Query, State},
|
||||
@@ -34,6 +35,11 @@ struct OsuCallbackQuery {
|
||||
state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RefreshTokenQuery {
|
||||
server: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OsuTokenResponse {
|
||||
access_token: String,
|
||||
@@ -178,7 +184,13 @@ async fn osu_callback(
|
||||
approved: None,
|
||||
season: None,
|
||||
};
|
||||
user_service.update(existing.id, update_request).await?
|
||||
let updated_user = user_service.update(existing.id, update_request).await?;
|
||||
|
||||
// 保存 OSU refresh_token
|
||||
user_service
|
||||
.update_osu_refresh_token(updated_user.id, token_response.refresh_token.clone())
|
||||
.await?;
|
||||
updated_user
|
||||
}
|
||||
None => {
|
||||
// 创建新用户
|
||||
@@ -190,7 +202,13 @@ async fn osu_callback(
|
||||
country: user_data.country.as_ref().map(|c| c.code.clone()),
|
||||
country_rank: user_data.statistics.as_ref().and_then(|s| s.country_rank),
|
||||
};
|
||||
user_service.create(user_data.id, create_request).await?
|
||||
let new_user = user_service.create(user_data.id, create_request).await?;
|
||||
|
||||
// 保存 OSU refresh_token
|
||||
user_service
|
||||
.update_osu_refresh_token(new_user.id, token_response.refresh_token.clone())
|
||||
.await?;
|
||||
new_user
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,20 +235,158 @@ async fn osu_callback(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/refresh",
|
||||
params(
|
||||
("server" = Option<String>, Query, description = "服务器类型,可选值:osu, gu,默认为osu"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "令牌刷新成功"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权或refresh_token过期"),
|
||||
(status = 500, description = "内部服务器错误"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn refresh_token() -> Result<Json<ApiResponse<AuthResponse>>> {
|
||||
// 实现令牌刷新逻辑
|
||||
// 这里需要从请求中获取refresh_token,然后调用OSU API刷新令牌
|
||||
// 暂时返回未实现
|
||||
Err(crate::error::AppError::NotImplemented(
|
||||
"Token refresh not implemented".into(),
|
||||
))
|
||||
async fn refresh_token(
|
||||
auth_user: AuthUser,
|
||||
Query(params): Query<RefreshTokenQuery>,
|
||||
State(state): State<ServiceState>,
|
||||
) -> Result<Json<ApiResponse<AuthResponse>>> {
|
||||
let server = params.server.unwrap_or_else(|| "osu".to_string());
|
||||
|
||||
if server != "osu" && server != "gu" {
|
||||
return Err(crate::error::AppError::ValidationError(
|
||||
"server参数必须是'osu'或'gu'".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let user_service = UserService::new(state.db);
|
||||
|
||||
// 根据服务器类型获取对应的refresh_token
|
||||
let refresh_token = match server.as_str() {
|
||||
"osu" => user_service.get_osu_refresh_token(auth_user.id).await?,
|
||||
"gu" => user_service.get_gu_refresh_token(auth_user.id).await?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let refresh_token = refresh_token.ok_or_else(|| {
|
||||
crate::error::AppError::Unauthorized(format!("{} refresh_token不存在", server))
|
||||
})?;
|
||||
|
||||
// 根据服务器类型调用相应的API刷新令牌
|
||||
let (client_id, client_secret, token_url, api_url) = match server.as_str() {
|
||||
"osu" => (
|
||||
env::var("OSU_CLIENT_ID").expect("OSU_CLIENT_ID not set"),
|
||||
env::var("OSU_CLIENT_SECRET").expect("OSU_CLIENT_SECRET not set"),
|
||||
"https://osu.ppy.sh/oauth/token",
|
||||
"https://osu.ppy.sh/api/v2/me",
|
||||
),
|
||||
"gu" => (
|
||||
env::var("GU_CLIENT_ID").expect("GU_CLIENT_ID not set"),
|
||||
env::var("GU_CLIENT_SECRET").expect("GU_CLIENT_SECRET not set"),
|
||||
"https://lazer-api.g0v0.top/oauth/token",
|
||||
"https://lazer-api.g0v0.top/api/v2/me",
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
// 使用refresh_token获取新的访问令牌
|
||||
let token_response = client
|
||||
.post(token_url)
|
||||
.json(&serde_json::json!({
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"refresh_token": refresh_token,
|
||||
"grant_type": "refresh_token"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// 如果refresh_token过期,返回401提示重新登录
|
||||
if e.status() == Some(reqwest::StatusCode::UNAUTHORIZED) {
|
||||
crate::error::AppError::Unauthorized(format!(
|
||||
"{} refresh_token已过期,请重新登录",
|
||||
server
|
||||
))
|
||||
} else {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to refresh token: {}", e))
|
||||
}
|
||||
})?
|
||||
.json::<OsuTokenResponse>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!(
|
||||
"Failed to parse token response: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// 获取用户信息
|
||||
let user_data = client
|
||||
.get(api_url)
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", token_response.access_token),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to get user info: {}", e))
|
||||
})?
|
||||
.json::<OsuUserData>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::AppError::ExternalApiError(format!("Failed to parse user info: {}", e))
|
||||
})?;
|
||||
|
||||
// 更新用户信息
|
||||
let update_request = crate::dto::user::UpdateUserRequest {
|
||||
username: Some(user_data.username.clone()),
|
||||
avatar_url: user_data.avatar_url,
|
||||
pp: user_data.statistics.as_ref().and_then(|s| s.pp),
|
||||
global_rank: user_data.statistics.as_ref().and_then(|s| s.global_rank),
|
||||
country: user_data.country.as_ref().map(|c| c.code.clone()),
|
||||
country_rank: user_data.statistics.as_ref().and_then(|s| s.country_rank),
|
||||
approved: None,
|
||||
season: None,
|
||||
};
|
||||
|
||||
let user = user_service.update(auth_user.id, update_request).await?;
|
||||
|
||||
// 更新数据库中的refresh_token
|
||||
match server.as_str() {
|
||||
"osu" => {
|
||||
user_service
|
||||
.update_osu_refresh_token(auth_user.id, token_response.refresh_token.clone())
|
||||
.await?;
|
||||
}
|
||||
"gu" => {
|
||||
user_service
|
||||
.update_gu_refresh_token(auth_user.id, token_response.refresh_token.clone())
|
||||
.await?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// 生成新的JWT令牌
|
||||
let osu_id = user.osu_id.clone();
|
||||
let user_group_str = match user.user_group {
|
||||
crate::entity::sea_orm_active_enums::UserGroup::Player => "player",
|
||||
crate::entity::sea_orm_active_enums::UserGroup::Admin => "admin",
|
||||
};
|
||||
let jwt_token =
|
||||
crate::utils::jwt::generate_token(osu_id, user_group_str.to_string()).map_err(|e| {
|
||||
crate::error::AppError::InternalError(format!("Failed to generate token: {}", e))
|
||||
})?;
|
||||
|
||||
let auth_response = AuthResponse {
|
||||
access_token: jwt_token,
|
||||
refresh_token: token_response.refresh_token,
|
||||
user: crate::dto::user::UserResponse::from(user),
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(auth_response)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -238,13 +394,25 @@ async fn refresh_token() -> Result<Json<ApiResponse<AuthResponse>>> {
|
||||
path = "/api/auth/logout",
|
||||
responses(
|
||||
(status = 200, description = "登出成功"),
|
||||
(status = 401, description = "未授权"),
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
async fn logout() -> Result<Json<ApiResponse<()>>> {
|
||||
// 实现登出逻辑
|
||||
// 这里需要清除客户端的令牌
|
||||
Ok(Json(ApiResponse::success_with_message((), "登出成功")))
|
||||
async fn logout(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<ServiceState>,
|
||||
) -> Result<Json<ApiResponse<()>>> {
|
||||
// 清除用户的refresh_token
|
||||
let user_service = UserService::new(state.db);
|
||||
|
||||
// 清除所有refresh_token(OSU和GU)
|
||||
user_service.clear_all_refresh_tokens(auth_user.id).await?;
|
||||
|
||||
// 返回成功消息
|
||||
Ok(Json(ApiResponse::success_with_message(
|
||||
(),
|
||||
"登出成功,已清除OSU和GU refresh_token",
|
||||
)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -340,6 +508,14 @@ async fn gu_callback(
|
||||
let updated_user_with_gu_info = user_service
|
||||
.update_gu_server_info(updated_user.id, gu_user_id, username.clone())
|
||||
.await?;
|
||||
|
||||
// 保存 GU refresh_token
|
||||
user_service
|
||||
.update_gu_refresh_token(
|
||||
updated_user_with_gu_info.id,
|
||||
token_response.refresh_token.clone(),
|
||||
)
|
||||
.await?;
|
||||
updated_user_with_gu_info
|
||||
}
|
||||
None => {
|
||||
@@ -359,6 +535,14 @@ async fn gu_callback(
|
||||
let new_user_with_gu_info = user_service
|
||||
.update_gu_server_info(new_user.id, gu_user_id, username.clone())
|
||||
.await?;
|
||||
|
||||
// 保存 GU refresh_token
|
||||
user_service
|
||||
.update_gu_refresh_token(
|
||||
new_user_with_gu_info.id,
|
||||
token_response.refresh_token.clone(),
|
||||
)
|
||||
.await?;
|
||||
new_user_with_gu_info
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ async fn get_tournament_settings(
|
||||
State(state): State<ServiceState>,
|
||||
Query(params): Query<TournamentSettingQueryParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedTournamentSettingsResponse>>> {
|
||||
let service = TournamentSettingService::new(state.db);
|
||||
let service = TournamentSettingService::new(state);
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let (settings, total) = service.find_all(params).await?;
|
||||
@@ -96,10 +96,10 @@ async fn update_tournament_settings(
|
||||
// 验证用户是否为admin
|
||||
require_admin(&auth_user)?;
|
||||
|
||||
let service = TournamentSettingService::new(state.db);
|
||||
let service = TournamentSettingService::new(state);
|
||||
|
||||
// 检查是否存在设置,如果不存在则初始化
|
||||
let settings = match service.get_current_settings().await? {
|
||||
let settings = match service.get_current_settings_cached().await? {
|
||||
Some(existing) => {
|
||||
// 更新现有设置
|
||||
service.update(existing.id, data).await?
|
||||
|
||||
157
src/api/user.rs
157
src/api/user.rs
@@ -23,13 +23,16 @@ pub fn routes() -> Router<ServiceState> {
|
||||
Router::new()
|
||||
.route("/", get(get_users))
|
||||
.route("/", post(create_user))
|
||||
.route("/{id}", get(get_user))
|
||||
.route("/{id}", put(update_user))
|
||||
.route("/{id}", delete(delete_user))
|
||||
.route("/{id}/user-group", put(update_user_group))
|
||||
.route("/{id}/registration-status", put(update_registration_status))
|
||||
.route("/{id}/season", put(update_season))
|
||||
.route("/{id}/approve", put(update_approved))
|
||||
.route("/{osu_id}", get(get_user))
|
||||
.route("/{osu_id}", put(update_user))
|
||||
.route("/{osu_id}", delete(delete_user))
|
||||
.route("/{osu_id}/user-group", put(update_user_group))
|
||||
.route(
|
||||
"/{osu_id}/registration-status",
|
||||
put(update_registration_status),
|
||||
)
|
||||
.route("/{osu_id}/season", put(update_season))
|
||||
.route("/{osu_id}/approve", put(update_approved))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -37,7 +40,7 @@ pub fn routes() -> Router<ServiceState> {
|
||||
path = "/api/users",
|
||||
params(UserQueryParams),
|
||||
responses(
|
||||
(status = 200, description = "获取用户列表成功", body = PaginatedUserResponse),
|
||||
(status = 200, description = "获取osu玩家列表成功", body = PaginatedUserResponse),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
),
|
||||
@@ -62,22 +65,25 @@ async fn get_users(
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{id}",
|
||||
path = "/api/users/{osu_id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "获取用户成功", body = UserResponse),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 200, description = "获取osu玩家成功", body = UserResponse),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn get_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.find_by_id(id).await?;
|
||||
let user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
@@ -87,80 +93,83 @@ async fn get_user(
|
||||
path = "/api/users",
|
||||
request_body = CreateUserRequest,
|
||||
responses(
|
||||
(status = 201, description = "创建用户成功", body = UserResponse),
|
||||
(status = 201, description = "创建osu玩家成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn create_user() -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
// 用户创建将通过OAuth回调处理
|
||||
// osu玩家创建将通过OAuth回调处理
|
||||
Err(crate::error::AppError::BusinessError(
|
||||
"请通过OAuth注册用户".into(),
|
||||
"请通过OAuth注册osu玩家".into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}",
|
||||
path = "/api/users/{osu_id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osu玩家ID")
|
||||
),
|
||||
request_body = UpdateUserRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新用户成功", body = UserResponse),
|
||||
(status = 200, description = "更新osu玩家成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
async fn update_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
auth_user: AuthUser,
|
||||
Json(data): Json<UpdateUserRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
// 检查是否为管理员,或者是否更新自己的信息
|
||||
// 注意:这里需要从数据库获取用户的osu_id来匹配
|
||||
// 暂时简化处理,实际项目中需要查询数据库
|
||||
let service = UserService::new(state.db);
|
||||
|
||||
// 获取要更新的osu玩家信息
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
// 检查权限:管理员或osu玩家本人可以更新
|
||||
let is_admin = auth_user.user_group == "admin";
|
||||
let is_self = auth_user.osu_id == id.to_string(); // 这里需要根据实际情况调整
|
||||
let is_self = auth_user.osu_id == target_user.osu_id;
|
||||
|
||||
if !is_admin && !is_self {
|
||||
return Err(crate::error::AppError::Forbidden(
|
||||
"只有管理员或用户本人可以更新用户信息".into(),
|
||||
));
|
||||
return Err(crate::error::AppError::Forbidden("权限错误".into()));
|
||||
}
|
||||
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update(id, data).await?;
|
||||
let user = service.update(target_user.id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/user-group",
|
||||
path = "/api/users/{osu_id}/user-group",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
request_body = UpdateUserGroupRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新用户组成功", body = UserResponse),
|
||||
(status = 200, description = "更新osu玩家组成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
async fn update_user_group(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
user: AuthUser,
|
||||
Json(data): Json<UpdateUserGroupRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
@@ -168,96 +177,121 @@ async fn update_user_group(
|
||||
require_admin(&user)?;
|
||||
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_user_group(id, data).await?;
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
let user = service.update_user_group(target_user.id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/users/{id}",
|
||||
path = "/api/users/{osu_id}",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "删除用户成功"),
|
||||
(status = 200, description = "删除osu玩家成功"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn delete_user(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
user: AuthUser,
|
||||
) -> Result<Json<ApiResponse<()>>> {
|
||||
// 检查是否为管理员
|
||||
require_admin(&user)?;
|
||||
|
||||
let service = UserService::new(state.db);
|
||||
service.delete(id).await?;
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
Ok(Json(ApiResponse::success_with_message((), "用户删除成功")))
|
||||
service.delete(target_user.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success_with_message(
|
||||
(),
|
||||
"osu玩家删除成功",
|
||||
)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/registration-status",
|
||||
path = "/api/users/{osu_id}/registration-status",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
request_body = UpdateRegistrationStatusRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新注册状态成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_registration_status(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
Json(data): Json<UpdateRegistrationStatusRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_registration_status(id, data).await?;
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
let user = service
|
||||
.update_registration_status(target_user.id, data)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/season",
|
||||
path = "/api/users/{osu_id}/season",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
request_body = UpdateSeasonRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新赛季成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
async fn update_season(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
Json(data): Json<UpdateSeasonRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_season(id, data).await?;
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
let user = service.update_season(target_user.id, data).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}/approve",
|
||||
path = "/api/users/{osu_id}/approve",
|
||||
params(
|
||||
("id" = i32, Path, description = "用户ID")
|
||||
("osu_id" = String, Path, description = "osuID")
|
||||
),
|
||||
request_body = UpdateApprovedRequest,
|
||||
responses(
|
||||
@@ -265,14 +299,14 @@ async fn update_season(
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未授权"),
|
||||
(status = 403, description = "权限不足"),
|
||||
(status = 404, description = "用户不存在"),
|
||||
(status = 404, description = "osu玩家不存在"),
|
||||
),
|
||||
tag = "users"
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
async fn update_approved(
|
||||
State(state): State<ServiceState>,
|
||||
Path(id): Path<i32>,
|
||||
Path(osu_id): Path<String>,
|
||||
user: AuthUser,
|
||||
Json(data): Json<UpdateApprovedRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>> {
|
||||
@@ -280,7 +314,12 @@ async fn update_approved(
|
||||
require_admin(&user)?;
|
||||
|
||||
let service = UserService::new(state.db);
|
||||
let user = service.update_approved(id, data, true).await?;
|
||||
let target_user = service
|
||||
.find_by_osu_id(&osu_id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::error::AppError::NotFound(format!("osu玩家 {} 不存在", osu_id)))?;
|
||||
|
||||
let user = service.update_approved(target_user.id, data, true).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ pub struct Config {
|
||||
|
||||
pub osu_redirect_uri: String,
|
||||
|
||||
pub gu_client_id: String,
|
||||
|
||||
pub gu_client_secret: String,
|
||||
|
||||
pub gu_redirect_uri: String,
|
||||
|
||||
pub cors_origin: String,
|
||||
|
||||
pub log_level: String,
|
||||
@@ -65,6 +71,16 @@ impl Config {
|
||||
crate::error::AppError::ConfigError("OSU_REDIRECT_URI未设置".into())
|
||||
})?,
|
||||
|
||||
gu_client_id: env::var("GU_CLIENT_ID")
|
||||
.map_err(|_| crate::error::AppError::ConfigError("GU_CLIENT_ID未设置".into()))?,
|
||||
|
||||
gu_client_secret: env::var("GU_CLIENT_SECRET").map_err(|_| {
|
||||
crate::error::AppError::ConfigError("GU_CLIENT_SECRET未设置".into())
|
||||
})?,
|
||||
|
||||
gu_redirect_uri: env::var("GU_REDIRECT_URI")
|
||||
.map_err(|_| crate::error::AppError::ConfigError("GU_REDIRECT_URI未设置".into()))?,
|
||||
|
||||
cors_origin: env::var("CORS_ORIGIN")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string()),
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct Model {
|
||||
pub registration_status: Option<RegistrationStatus>,
|
||||
pub gu_server_user_id: Option<i32>,
|
||||
pub gu_server_username: Option<String>,
|
||||
pub osu_refresh_token: Option<String>,
|
||||
pub gu_refresh_token: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@ pub mod tournament_settings;
|
||||
pub mod user;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 服务层共享状态
|
||||
#[derive(Clone)]
|
||||
pub struct ServiceState {
|
||||
pub db: DatabaseConnection,
|
||||
pub tournament_settings_cache: Arc<RwLock<Option<crate::entity::tournament_settings::Model>>>,
|
||||
}
|
||||
|
||||
/// 确保 ServiceState 实现了 Send + Sync traits
|
||||
@@ -23,6 +26,9 @@ unsafe impl Sync for ServiceState {}
|
||||
impl ServiceState {
|
||||
/// 创建新的服务状态
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
Self {
|
||||
db,
|
||||
tournament_settings_cache: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
dto::tournament_settings::{
|
||||
@@ -10,15 +12,20 @@ use crate::{
|
||||
},
|
||||
entity::tournament_settings,
|
||||
error::{AppError, Result},
|
||||
service::ServiceState,
|
||||
};
|
||||
|
||||
pub struct TournamentSettingService {
|
||||
db: DatabaseConnection,
|
||||
current_settings_cache: Arc<RwLock<Option<tournament_settings::Model>>>,
|
||||
}
|
||||
|
||||
impl TournamentSettingService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
pub fn new(state: ServiceState) -> Self {
|
||||
Self {
|
||||
db: state.db,
|
||||
current_settings_cache: state.tournament_settings_cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_all(
|
||||
@@ -71,7 +78,13 @@ impl TournamentSettingService {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(tournament_setting.insert(&self.db).await?)
|
||||
let result = tournament_setting.insert(&self.db).await?;
|
||||
|
||||
// 更新缓存
|
||||
let mut cache = self.current_settings_cache.write().await;
|
||||
*cache = Some(result.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
@@ -108,10 +121,29 @@ impl TournamentSettingService {
|
||||
|
||||
tournament_setting.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(tournament_setting.update(&self.db).await?)
|
||||
let result = tournament_setting.update(&self.db).await?;
|
||||
|
||||
// 更新缓存
|
||||
let mut cache = self.current_settings_cache.write().await;
|
||||
*cache = Some(result.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: i32) -> Result<()> {
|
||||
// 检查缓存中是否有这个项目
|
||||
{
|
||||
let cache = self.current_settings_cache.read().await;
|
||||
if let Some(cached_settings) = cache.as_ref() {
|
||||
if cached_settings.id == id {
|
||||
// 删除的是缓存的项目,清除缓存
|
||||
drop(cache); // 释放读锁
|
||||
let mut cache = self.current_settings_cache.write().await;
|
||||
*cache = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = tournament_settings::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
@@ -127,4 +159,33 @@ impl TournamentSettingService {
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_current_settings_cached(&self) -> Result<Option<tournament_settings::Model>> {
|
||||
// 首先尝试从缓存读取
|
||||
{
|
||||
let cache = self.current_settings_cache.read().await;
|
||||
if let Some(settings) = cache.as_ref() {
|
||||
return Ok(Some(settings.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库读取
|
||||
let settings = tournament_settings::Entity::find()
|
||||
.order_by_desc(tournament_settings::Column::CreatedAt)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
// 更新缓存
|
||||
if let Some(ref settings) = settings {
|
||||
let mut cache = self.current_settings_cache.write().await;
|
||||
*cache = Some(settings.clone());
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub async fn invalidate_cache(&self) {
|
||||
let mut cache = self.current_settings_cache.write().await;
|
||||
*cache = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ use sea_orm::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dto::user::{CreateUserRequest, UpdateApprovedRequest, UpdateRegistrationStatusRequest, UpdateSeasonRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams},
|
||||
entity::{sea_orm_active_enums::{RegistrationStatus, UserGroup}, user},
|
||||
dto::user::{
|
||||
CreateUserRequest, UpdateApprovedRequest, UpdateRegistrationStatusRequest,
|
||||
UpdateSeasonRequest, UpdateUserGroupRequest, UpdateUserRequest, UserQueryParams,
|
||||
},
|
||||
entity::{
|
||||
sea_orm_active_enums::{RegistrationStatus, UserGroup},
|
||||
user,
|
||||
},
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
@@ -165,7 +171,11 @@ impl UserService {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update_registration_status(&self, id: i32, data: UpdateRegistrationStatusRequest) -> Result<user::Model> {
|
||||
pub async fn update_registration_status(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateRegistrationStatusRequest,
|
||||
) -> Result<user::Model> {
|
||||
let status_enum = match data.registration_status.as_str() {
|
||||
"pending" => RegistrationStatus::Pending,
|
||||
"confirmed" => RegistrationStatus::Confirmed,
|
||||
@@ -188,7 +198,12 @@ impl UserService {
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_approved(&self, id: i32, data: UpdateApprovedRequest, is_admin: bool) -> Result<user::Model> {
|
||||
pub async fn update_approved(
|
||||
&self,
|
||||
id: i32,
|
||||
data: UpdateApprovedRequest,
|
||||
is_admin: bool,
|
||||
) -> Result<user::Model> {
|
||||
if !is_admin {
|
||||
return Err(AppError::Forbidden("只有管理员可以更新审批状态".into()));
|
||||
}
|
||||
@@ -208,7 +223,12 @@ impl UserService {
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_gu_server_info(&self, id: i32, gu_user_id: i32, gu_username: String) -> Result<user::Model> {
|
||||
pub async fn update_gu_server_info(
|
||||
&self,
|
||||
id: i32,
|
||||
gu_user_id: i32,
|
||||
gu_username: String,
|
||||
) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.gu_server_user_id = Set(Some(gu_user_id));
|
||||
user.gu_server_username = Set(Some(gu_username));
|
||||
@@ -216,4 +236,63 @@ impl UserService {
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_osu_refresh_token(
|
||||
&self,
|
||||
id: i32,
|
||||
refresh_token: String,
|
||||
) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.osu_refresh_token = Set(Some(refresh_token));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_gu_refresh_token(
|
||||
&self,
|
||||
id: i32,
|
||||
refresh_token: String,
|
||||
) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.gu_refresh_token = Set(Some(refresh_token));
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_osu_refresh_token(&self, id: i32) -> Result<Option<String>> {
|
||||
let user = self.find_by_id(id).await?;
|
||||
Ok(user.osu_refresh_token)
|
||||
}
|
||||
|
||||
pub async fn get_gu_refresh_token(&self, id: i32) -> Result<Option<String>> {
|
||||
let user = self.find_by_id(id).await?;
|
||||
Ok(user.gu_refresh_token)
|
||||
}
|
||||
|
||||
pub async fn clear_osu_refresh_token(&self, id: i32) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.osu_refresh_token = Set(None);
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn clear_gu_refresh_token(&self, id: i32) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.gu_refresh_token = Set(None);
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn clear_all_refresh_tokens(&self, id: i32) -> Result<user::Model> {
|
||||
let mut user: user::ActiveModel = self.find_by_id(id).await?.into();
|
||||
user.osu_refresh_token = Set(None);
|
||||
user.gu_refresh_token = Set(None);
|
||||
user.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(user.update(&self.db).await?)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user