From d76bf5b7519ba40dc6a2066b7469b180139b5bab Mon Sep 17 00:00:00 2001 From: shiyu Date: Sun, 1 Jun 2025 15:05:26 +0800 Subject: [PATCH] feat: implement account binding functionality for GitHub and LinuxDo --- Api/AuthController.cs | 82 ++++++- Models/DataBase/User.cs | 2 + Models/Request/Auth/BindAccountRequest.cs | 25 ++ Services/Auth/AuthService.cs | 282 ++++++++++++++++++---- Services/Auth/GitHubAuthResult.cs | 11 + Services/Auth/IAuthService.cs | 9 +- Services/Auth/LinuxDoAuthResult.cs | 11 + Web/public/images/linuxdo.svg | 1 + Web/src/App.tsx | 3 +- Web/src/api/authApi.ts | 25 +- Web/src/api/index.ts | 5 +- Web/src/api/types.ts | 14 ++ Web/src/pages/admin/system/ConfigTabs.tsx | 23 ++ Web/src/pages/admin/system/Index.tsx | 5 +- Web/src/pages/bind/Index.tsx | 214 ++++++++++++++++ Web/src/pages/login/Index.tsx | 11 +- Web/src/pages/register/Index.tsx | 11 +- 17 files changed, 670 insertions(+), 64 deletions(-) create mode 100644 Models/Request/Auth/BindAccountRequest.cs create mode 100644 Services/Auth/GitHubAuthResult.cs create mode 100644 Services/Auth/LinuxDoAuthResult.cs create mode 100644 Web/public/images/linuxdo.svg create mode 100644 Web/src/pages/bind/Index.tsx diff --git a/Api/AuthController.cs b/Api/AuthController.cs index 77723cc..98c2be7 100644 --- a/Api/AuthController.cs +++ b/Api/AuthController.cs @@ -9,7 +9,7 @@ using Foxel.Services.Configuration; namespace Foxel.Controllers; [Route("api/auth")] -public class AuthController(IAuthService authService, IConfigService configService) : BaseApiController +public class AuthController(IAuthService authService) : BaseApiController { [HttpPost("register")] public async Task>> Register([FromBody] RegisterRequest request) @@ -108,14 +108,50 @@ public class AuthController(IAuthService authService, IConfigService configServi [HttpGet("github/callback")] public async Task>> GitHubCallback(string code) { - var (success, message, token) = await authService.ProcessGitHubCallbackAsync(code); - - if (!success || token == null) + var (result, message, data) = await authService.ProcessGitHubCallbackAsync(code); + + switch (result) { - return Redirect($"/login?error=github_auth_failed&message={Uri.EscapeDataString(message)}"); + case GitHubAuthResult.Success: + return Redirect($"/login?token={Uri.EscapeDataString(data!)}"); + + case GitHubAuthResult.UserNotBound: + return Redirect($"/bind?githubId={data}"); + case GitHubAuthResult.InvalidCode: + case GitHubAuthResult.TokenRequestFailed: + case GitHubAuthResult.UserInfoFailed: + case GitHubAuthResult.InvalidUserId: + default: + return Redirect($"/login?error=github_auth_failed&message={Uri.EscapeDataString(message)}"); + } + } + + [HttpGet("linuxdo/login")] + public IActionResult LinuxDoLogin() + { + string linuxdoAuthorizeUrl = authService.GetLinuxDoLoginUrl(); + return Redirect(linuxdoAuthorizeUrl); + } + + [HttpGet("linuxdo/callback")] + public async Task>> LinuxDoCallback(string code, string state) + { + var (result, message, data) = await authService.ProcessLinuxDoCallbackAsync(code); + + switch (result) + { + case LinuxDoAuthResult.Success: + return Redirect($"/login?token={Uri.EscapeDataString(data!)}"); + + case LinuxDoAuthResult.UserNotBound: + return Redirect($"/bind?linuxdoId={data}"); + case LinuxDoAuthResult.InvalidCode: + case LinuxDoAuthResult.TokenRequestFailed: + case LinuxDoAuthResult.UserInfoFailed: + case LinuxDoAuthResult.InvalidUserId: + default: + return Redirect($"/login?error=linuxdo_auth_failed&message={Uri.EscapeDataString(message)}"); } - - return Redirect($"/login?token={Uri.EscapeDataString(token)}"); } [HttpPut("update")] @@ -126,7 +162,7 @@ public class AuthController(IAuthService authService, IConfigService configServi { return Error("请求数据无效"); } - + var userId = GetCurrentUserId(); if (userId == null) { @@ -149,4 +185,34 @@ public class AuthController(IAuthService authService, IConfigService configServi return Success(profile, "用户信息更新成功"); } + + [HttpPost("bind")] + public async Task>> BindAccount([FromBody] BindAccountRequest request) + { + if (!ModelState.IsValid) + { + return Error("请求数据无效"); + } + + var (success, message, user) = await authService.BindAccountAsync(request); + if (!success || user == null) + { + return Error(message); + } + + var token = await authService.GenerateJwtTokenAsync(user); + var response = new AuthResponse + { + Token = token, + User = new UserProfile + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + RoleName = user.Role?.Name + } + }; + + return Success(response, message); + } } \ No newline at end of file diff --git a/Models/DataBase/User.cs b/Models/DataBase/User.cs index 6da5feb..3541822 100644 --- a/Models/DataBase/User.cs +++ b/Models/DataBase/User.cs @@ -14,6 +14,8 @@ public class User : BaseModel [Required] [StringLength(255)] public required string PasswordHash { get; set; } [StringLength(255)] public string? GithubId { get; set; } + [StringLength(255)] public string? LinuxDoId { get; set; } + public int? RoleId { get; set; } public Role? Role { get; set; } diff --git a/Models/Request/Auth/BindAccountRequest.cs b/Models/Request/Auth/BindAccountRequest.cs new file mode 100644 index 0000000..ef7d9d4 --- /dev/null +++ b/Models/Request/Auth/BindAccountRequest.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace Foxel.Models.Request.Auth; + +public class BindAccountRequest +{ + [Required(ErrorMessage = "邮箱不能为空")] + [EmailAddress(ErrorMessage = "邮箱格式不正确")] + public string Email { get; set; } = string.Empty; + + [Required(ErrorMessage = "密码不能为空")] + [MinLength(6, ErrorMessage = "密码长度不能少于6位")] + public string Password { get; set; } = string.Empty; + + [Required(ErrorMessage = "绑定类型不能为空")] public BindType BindType { get; set; } + + [Required(ErrorMessage = "第三方用户ID不能为空")] + public string ThirdPartyUserId { get; set; } = string.Empty; +} + +public enum BindType +{ + GitHub, + LinuxDo +} diff --git a/Services/Auth/AuthService.cs b/Services/Auth/AuthService.cs index c6c9327..9a70dd9 100644 --- a/Services/Auth/AuthService.cs +++ b/Services/Auth/AuthService.cs @@ -27,6 +27,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { return (false, "该用户名已被使用", null); } + var user = new User { UserName = request.UserName, @@ -44,6 +45,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi user.RoleId = 1; user.Role = role; } + context.Users.Add(user); await context.SaveChangesAsync(); return (true, "用户注册成功", user); @@ -103,45 +105,34 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi return await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId); } - public async Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync( - string githubId, string? githubName, string? email) + public async Task<(bool success, string message, User? user)> FindGitHubUserAsync(string githubId) { - if (string.IsNullOrEmpty(email)) - { - return (false, "GitHub账号未提供邮箱地址", null); - } - await using var context = await dbContextFactory.CreateDbContextAsync(); - - var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Email == email); + var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.GithubId == githubId); if (user == null) { - user = new User - { - UserName = $"{githubName}", - Email = email, - PasswordHash = HashPassword(Guid.NewGuid().ToString()), - GithubId = githubId, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - context.Users.Add(user); - await context.SaveChangesAsync(); - return (true, "GitHub用户注册成功", user); + return (false, "未找到对应的GitHub用户", null); } - if (string.IsNullOrEmpty(user.GithubId)) - { - user.GithubId = githubId; - user.UpdatedAt = DateTime.UtcNow; - await context.SaveChangesAsync(); - } - - return (true, "GitHub用户登录成功", user); + return (true, "找到GitHub用户", user); } - public async Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, UpdateUserRequest request) + public async Task<(bool success, string message, User? user)> FindLinuxDoUserAsync(string linuxdoId) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.LinuxDoId == linuxdoId); + + if (user == null) + { + return (false, "未找到对应的LinuxDo用户", null); + } + + return (true, "找到LinuxDo用户", user); + } + + public async Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, + UpdateUserRequest request) { await using var context = await dbContextFactory.CreateDbContextAsync(); var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId); @@ -159,6 +150,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { return (false, "该用户名已被使用", null); } + user.UserName = request.UserName; } @@ -170,6 +162,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { return (false, "该邮箱已被注册", null); } + user.Email = request.Email; } @@ -199,14 +192,15 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { string githubClientId = configuration["Authentication:GitHubClientId"]; string githubCallback = configuration["Authentication:GitHubCallbackUrl"]; - return $"https://github.com/login/oauth/authorize?client_id={Uri.EscapeDataString(githubClientId)}&redirect_uri={Uri.EscapeDataString(githubCallback)}"; + return + $"https://github.com/login/oauth/authorize?client_id={Uri.EscapeDataString(githubClientId)}&redirect_uri={Uri.EscapeDataString(githubCallback)}"; } - public async Task<(bool success, string message, string? token)> ProcessGitHubCallbackAsync(string code) + public async Task<(GitHubAuthResult result, string message, string? data)> ProcessGitHubCallbackAsync(string code) { if (string.IsNullOrEmpty(code)) { - return (false, "GitHub授权码无效", null); + return (GitHubAuthResult.InvalidCode, "GitHub授权码无效", null); } string githubClientId = configuration["Authentication:GitHubClientId"]; @@ -225,7 +219,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { var errorContent = await tokenResponse.Content.ReadAsStringAsync(); Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}"); - return (false, $"获取GitHub访问令牌失败: {errorContent}", null); + return (GitHubAuthResult.TokenRequestFailed, $"获取GitHub访问令牌失败: {errorContent}", null); } var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync(); @@ -235,7 +229,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi accessTokenElement.GetString() == null) { Console.WriteLine($"GitHub响应中未找到access_token: {tokenResponseContent}"); - return (false, "获取GitHub访问令牌失败,响应中未包含令牌。", null); + return (GitHubAuthResult.TokenRequestFailed, "获取GitHub访问令牌失败,响应中未包含令牌。", null); } var accessToken = accessTokenElement.GetString(); @@ -248,7 +242,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi { var errorContent = await userResponse.Content.ReadAsStringAsync(); Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}"); - return (false, $"获取GitHub用户信息失败: {errorContent}", null); + return (GitHubAuthResult.UserInfoFailed, $"获取GitHub用户信息失败: {errorContent}", null); } var userContent = await userResponse.Content.ReadAsStringAsync(); @@ -281,18 +275,224 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (string.IsNullOrEmpty(githubUserId)) { - return (false, "无法从GitHub获取用户ID", null); + return (GitHubAuthResult.InvalidUserId, "无法从GitHub获取用户ID", null); } - var (isSuccess, message, user) = await FindOrCreateGitHubUserAsync(githubUserId, name ?? loginName, email); + var (isSuccess, message, user) = await FindGitHubUserAsync(githubUserId); if (!isSuccess || user == null) { - Console.WriteLine($"创建或查找GitHub用户失败: {message}"); - return (false, message, null); + return (GitHubAuthResult.UserNotBound, "GitHub用户未绑定到系统账户", githubUserId); } var jwtToken = await GenerateJwtTokenAsync(user); - return (true, "GitHub授权成功", jwtToken); + return (GitHubAuthResult.Success, "GitHub授权成功", jwtToken); + } + + public string GetLinuxDoLoginUrl() + { + string linuxdoClientId = configuration["Authentication:LinuxDoClientId"]; + string linuxdoCallback = configuration["Authentication:LinuxDoCallbackUrl"]; + string state = Guid.NewGuid().ToString(); + return + $"https://connect.linux.do/oauth2/authorize?response_type=code&client_id={Uri.EscapeDataString(linuxdoClientId)}&redirect_uri={Uri.EscapeDataString(linuxdoCallback)}&state={Uri.EscapeDataString(state)}"; + } + + public async Task<(LinuxDoAuthResult result, string message, string? data)> ProcessLinuxDoCallbackAsync(string code) + { + if (string.IsNullOrEmpty(code)) + { + return (LinuxDoAuthResult.InvalidCode, "LinuxDo授权码无效", null); + } + + string linuxdoClientId = configuration["Authentication:LinuxDoClientId"]; + string linuxdoClientSecret = configuration["Authentication:LinuxDoClientSecret"]; + string linuxdoCallback = configuration["Authentication:LinuxDoCallbackUrl"]; + string linuxdoTokenUrl = "https://connect.linux.do/oauth2/token"; + string linuxdoUserApiUrl = "https://connect.linux.do/api/user"; + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "Foxel"); + + // 构建 token 请求参数 + var tokenParams = new List> + { + new("grant_type", "authorization_code"), + new("client_id", linuxdoClientId), + new("client_secret", linuxdoClientSecret), + new("code", code), + new("redirect_uri", linuxdoCallback) + }; + + var tokenContent = new FormUrlEncodedContent(tokenParams); + var tokenResponse = await httpClient.PostAsync(linuxdoTokenUrl, tokenContent); + + if (!tokenResponse.IsSuccessStatusCode) + { + var errorContent = await tokenResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"获取LinuxDo访问令牌失败: {tokenResponse.StatusCode}, {errorContent}"); + return (LinuxDoAuthResult.TokenRequestFailed, $"获取LinuxDo访问令牌失败: {errorContent}", null); + } + + var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync(); + var tokenJson = System.Text.Json.JsonDocument.Parse(tokenResponseContent); + + if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) || + accessTokenElement.GetString() == null) + { + Console.WriteLine($"LinuxDo响应中未找到access_token: {tokenResponseContent}"); + return (LinuxDoAuthResult.TokenRequestFailed, "获取LinuxDo访问令牌失败,响应中未包含令牌。", null); + } + + var accessToken = accessTokenElement.GetString(); + + // 获取用户信息 + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var userResponse = await httpClient.GetAsync(linuxdoUserApiUrl); + if (!userResponse.IsSuccessStatusCode) + { + var errorContent = await userResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"获取LinuxDo用户信息失败: {userResponse.StatusCode}, {errorContent}"); + return (LinuxDoAuthResult.UserInfoFailed, $"获取LinuxDo用户信息失败: {errorContent}", null); + } + + var userContent = await userResponse.Content.ReadAsStringAsync(); + var userJson = System.Text.Json.JsonDocument.Parse(userContent); + + string? linuxdoUserId = null; + string? email = null; + string? username = null; + + if (userJson.RootElement.TryGetProperty("id", out var idElement)) + { + linuxdoUserId = idElement.GetInt32().ToString(); + } + + if (userJson.RootElement.TryGetProperty("email", out var emailElement)) + { + email = emailElement.GetString(); + } + + if (userJson.RootElement.TryGetProperty("username", out var usernameElement)) + { + username = usernameElement.GetString(); + } + + if (string.IsNullOrEmpty(linuxdoUserId)) + { + return (LinuxDoAuthResult.InvalidUserId, "无法从LinuxDo获取用户ID", null); + } + + var (isSuccess, message, user) = await FindLinuxDoUserAsync(linuxdoUserId); + + if (!isSuccess || user == null) + { + return (LinuxDoAuthResult.UserNotBound, "LinuxDo用户未绑定到系统账户", linuxdoUserId); + } + + var jwtToken = await GenerateJwtTokenAsync(user); + return (LinuxDoAuthResult.Success, "LinuxDo授权成功", jwtToken); + } + + public async Task<(bool success, string message, User? user)> BindAccountAsync(BindAccountRequest request) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + + // 检查第三方ID是否已被绑定 + User? existingThirdPartyUser = null; + if (request.BindType == BindType.GitHub) + { + existingThirdPartyUser = await context.Users.Include(x => x.Role) + .FirstOrDefaultAsync(u => u.GithubId == request.ThirdPartyUserId); + } + else if (request.BindType == BindType.LinuxDo) + { + existingThirdPartyUser = await context.Users.Include(x => x.Role) + .FirstOrDefaultAsync(u => u.LinuxDoId == request.ThirdPartyUserId); + } + + if (existingThirdPartyUser != null) + { + return (false, $"该{request.BindType}账户已被绑定", null); + } + + // 查找邮箱对应的用户 + var existingUser = await context.Users.Include(x => x.Role) + .FirstOrDefaultAsync(u => u.Email == request.Email); + + if (existingUser != null) + { + // 验证密码 + if (!VerifyPassword(request.Password, existingUser.PasswordHash)) + { + return (false, "密码错误", null); + } + + // 检查是否已绑定对应类型的第三方账户 + if (request.BindType == BindType.GitHub && !string.IsNullOrEmpty(existingUser.GithubId)) + { + return (false, "该账户已绑定GitHub", null); + } + + if (request.BindType == BindType.LinuxDo && !string.IsNullOrEmpty(existingUser.LinuxDoId)) + { + return (false, "该账户已绑定LinuxDo", null); + } + + // 绑定第三方账户 + if (request.BindType == BindType.GitHub) + { + existingUser.GithubId = request.ThirdPartyUserId; + } + else if (request.BindType == BindType.LinuxDo) + { + existingUser.LinuxDoId = request.ThirdPartyUserId; + } + + existingUser.UpdatedAt = DateTime.UtcNow; + await context.SaveChangesAsync(); + + return (true, $"{request.BindType}账户绑定成功", existingUser); + } + else + { + // 用户不存在,创建新用户并绑定第三方账户 + var newUser = new User + { + UserName = request.Email.Split('@')[0], // 使用邮箱前缀作为用户名 + Email = request.Email, + PasswordHash = HashPassword(request.Password), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + RoleId = 2, // 默认用户角色 + Role = null, + }; + + // 绑定第三方账户 + if (request.BindType == BindType.GitHub) + { + newUser.GithubId = request.ThirdPartyUserId; + } + else if (request.BindType == BindType.LinuxDo) + { + newUser.LinuxDoId = request.ThirdPartyUserId; + } + + // 如果是第一个用户,设置为管理员 + var userCount = await context.Users.CountAsync(); + if (userCount == 0) + { + var role = await context.Roles.FirstOrDefaultAsync(r => r.Name == "Administrator"); + newUser.RoleId = 1; + newUser.Role = role; + } + + context.Users.Add(newUser); + await context.SaveChangesAsync(); + + return (true, $"账户注册并绑定{request.BindType}成功", newUser); + } } } \ No newline at end of file diff --git a/Services/Auth/GitHubAuthResult.cs b/Services/Auth/GitHubAuthResult.cs new file mode 100644 index 0000000..5cf1027 --- /dev/null +++ b/Services/Auth/GitHubAuthResult.cs @@ -0,0 +1,11 @@ +namespace Foxel.Services.Auth; + +public enum GitHubAuthResult +{ + Success, // 授权成功并找到绑定用户 + UserNotBound, // 授权成功但用户未绑定 + InvalidCode, // 授权码无效 + TokenRequestFailed, // 获取访问令牌失败 + UserInfoFailed, // 获取用户信息失败 + InvalidUserId // 无法获取GitHub用户ID +} diff --git a/Services/Auth/IAuthService.cs b/Services/Auth/IAuthService.cs index 15155ee..1a68026 100644 --- a/Services/Auth/IAuthService.cs +++ b/Services/Auth/IAuthService.cs @@ -9,9 +9,12 @@ public interface IAuthService Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request); Task GenerateJwtTokenAsync(User user); Task GetUserByIdAsync(int userId); - Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync( - string githubId, string? githubName, string? email); + Task<(bool success, string message, User? user)> FindGitHubUserAsync(string githubId); + Task<(bool success, string message, User? user)> FindLinuxDoUserAsync(string linuxdoId); Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, UpdateUserRequest request); string GetGitHubLoginUrl(); - Task<(bool success, string message, string? token)> ProcessGitHubCallbackAsync(string code); + string GetLinuxDoLoginUrl(); + Task<(GitHubAuthResult result, string message, string? data)> ProcessGitHubCallbackAsync(string code); + Task<(LinuxDoAuthResult result, string message, string? data)> ProcessLinuxDoCallbackAsync(string code); + Task<(bool success, string message, User? user)> BindAccountAsync(BindAccountRequest request); } diff --git a/Services/Auth/LinuxDoAuthResult.cs b/Services/Auth/LinuxDoAuthResult.cs new file mode 100644 index 0000000..87817ef --- /dev/null +++ b/Services/Auth/LinuxDoAuthResult.cs @@ -0,0 +1,11 @@ +namespace Foxel.Services.Auth; + +public enum LinuxDoAuthResult +{ + Success, // 授权成功并找到绑定用户 + UserNotBound, // 授权成功但用户未绑定 + InvalidCode, // 授权码无效 + TokenRequestFailed, // 获取访问令牌失败 + UserInfoFailed, // 获取用户信息失败 + InvalidUserId // 无法获取LinuxDo用户ID +} diff --git a/Web/public/images/linuxdo.svg b/Web/public/images/linuxdo.svg new file mode 100644 index 0000000..fd4dedd --- /dev/null +++ b/Web/public/images/linuxdo.svg @@ -0,0 +1 @@ +LINUX DO Logo \ No newline at end of file diff --git a/Web/src/App.tsx b/Web/src/App.tsx index cabb9dd..62208ba 100644 --- a/Web/src/App.tsx +++ b/Web/src/App.tsx @@ -10,6 +10,7 @@ import { getMainRoutes, getAdminRoutes } from './routes'; import { AuthProvider } from './auth/AuthContext'; import AnonymousPage from './pages/anonymous/Index'; import AdminLayout from './layouts/AdminLayout'; +import Bind from './pages/bind/Index'; const PrivateRoute = ({ children }: { children: JSX.Element }) => { return isAuthenticated() ? children : ; @@ -56,8 +57,8 @@ function App() { } /> } /> + } /> } /> - diff --git a/Web/src/api/authApi.ts b/Web/src/api/authApi.ts index 727aa44..a7bd08f 100644 --- a/Web/src/api/authApi.ts +++ b/Web/src/api/authApi.ts @@ -1,4 +1,4 @@ -import {type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile, type UpdateUserRequest} from './types'; +import {type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile, type UpdateUserRequest, type BindAccountRequest} from './types'; import {fetchApi, BASE_URL} from './fetchClient'; // 认证数据本地存储键 @@ -89,6 +89,22 @@ export async function updateUserInfo(data: UpdateUserRequest): Promise> { + const response = await fetchApi('/auth/bind', { + method: 'POST', + body: JSON.stringify(data), + }); + + if (response.success && response.data) { + clearAuthData(); // 清除旧的认证数据 + console.log('绑定成功,保存认证数据:', response.data); + saveAuthData(response.data); // 保存新的认证数据 + } + + return response; +} + // 保存认证数据到本地存储 export const saveAuthData = (authData: AuthResponse): void => { localStorage.setItem(TOKEN_KEY, authData.token); @@ -150,7 +166,6 @@ export async function handleOAuthCallback(): Promise { saveAuthData(authResponse); - // 清除URL中的token参数 const url = new URL(window.location.href); url.searchParams.delete('token'); window.history.replaceState({}, document.title, url.toString()); @@ -160,7 +175,7 @@ export async function handleOAuthCallback(): Promise { return false; } catch (error) { console.error('第三方登录处理失败:', error); - clearAuthData(); // 清除可能部分保存的数据 + clearAuthData(); return false; } } @@ -170,4 +185,8 @@ export async function handleOAuthCallback(): Promise { export function getGitHubLoginUrl(): string { return `${BASE_URL}/auth/github/login`; +} + +export function getLinuxDoLoginUrl(): string { + return `${BASE_URL}/auth/linuxdo/login`; } \ No newline at end of file diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 8b6f403..42ecdce 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -14,7 +14,10 @@ export { saveAuthData, clearAuthData, isAuthenticated, - getStoredUser + getStoredUser, + bindAccount, + getGitHubLoginUrl, + getLinuxDoLoginUrl } from './authApi'; // 导出Picture API diff --git a/Web/src/api/types.ts b/Web/src/api/types.ts index f2db0ac..77e57e4 100644 --- a/Web/src/api/types.ts +++ b/Web/src/api/types.ts @@ -271,3 +271,17 @@ export const VectorDbType = { export interface VectorDbInfo { type: string; } + +export type BindType = 0 | 1; + +export const BindType = { + GitHub: 0 as BindType, + LinuxDo: 1 as BindType, +}; + +export interface BindAccountRequest { + email: string; + password: string; + bindType: BindType; + thirdPartyUserId: string; +} diff --git a/Web/src/pages/admin/system/ConfigTabs.tsx b/Web/src/pages/admin/system/ConfigTabs.tsx index 565661e..f182219 100644 --- a/Web/src/pages/admin/system/ConfigTabs.tsx +++ b/Web/src/pages/admin/system/ConfigTabs.tsx @@ -251,6 +251,29 @@ const ConfigTabs: React.FC = ({ + + } + description="LinuxDo OAuth 应用配置,用于实现第三方登录功能" + isMobile={isMobile} + > +
+ {renderConfigFormItems(formsMap.Authentication, "Authentication", ["LinuxDoClientId", "LinuxDoClientSecret", "LinuxDoCallbackUrl"])} + + + + + +
+
) }, diff --git a/Web/src/pages/admin/system/Index.tsx b/Web/src/pages/admin/system/Index.tsx index d0a54b9..250421c 100644 --- a/Web/src/pages/admin/system/Index.tsx +++ b/Web/src/pages/admin/system/Index.tsx @@ -37,7 +37,10 @@ const allDescriptions: Record> = { Authentication: { GitHubClientId: 'GitHub OAuth 应用客户端ID', GitHubClientSecret: 'GitHub OAuth 应用客户端密钥', - GitHubCallbackUrl: 'GitHub OAuth 认证回调地址' + GitHubCallbackUrl: 'GitHub OAuth 认证回调地址', + LinuxDoClientId: 'LinuxDo OAuth 应用客户端ID', + LinuxDoClientSecret: 'LinuxDo OAuth 应用客户端密钥', + LinuxDoCallbackUrl: 'LinuxDo OAuth 认证回调地址' }, AppSettings: { ServerUrl: '服务器URL' diff --git a/Web/src/pages/bind/Index.tsx b/Web/src/pages/bind/Index.tsx new file mode 100644 index 0000000..d57f493 --- /dev/null +++ b/Web/src/pages/bind/Index.tsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect } from 'react'; +import { Form, Input, Button, Typography, Row, Col, Card, message, Alert } from 'antd'; +import { UserOutlined, LockOutlined, GithubOutlined, LinkOutlined } from '@ant-design/icons'; +import { useNavigate, useSearchParams } from 'react-router'; +import { bindAccount, BindType } from '../../api'; +import useIsMobile from '../../hooks/useIsMobile'; + +const { Title, Text } = Typography; + +const Bind: React.FC = () => { + const [loading, setLoading] = useState(false); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const isMobile = useIsMobile(); + + const githubId = searchParams.get('githubId'); + const linuxdoId = searchParams.get('linuxdoId'); + const thirdPartyUserId = githubId || linuxdoId; + const bindType = githubId ? BindType.GitHub : BindType.LinuxDo; + + useEffect(() => { + // 检查是否有必要的参数 + if (!thirdPartyUserId) { + message.error('缺少必要的绑定参数'); + navigate('/login'); + } + }, [thirdPartyUserId, navigate]); + + const onFinish = async (values: any) => { + if (!thirdPartyUserId) { + message.error('缺少第三方用户ID'); + return; + } + + setLoading(true); + try { + const response = await bindAccount({ + email: values.email, + password: values.password, + bindType: bindType, + thirdPartyUserId: thirdPartyUserId + }); + + if (response.success && response.data) { + message.success(response.message || '账户绑定成功!'); + navigate('/'); + } else { + message.error(response.message || '绑定失败,请检查邮箱和密码'); + } + } catch (error) { + console.error('绑定出错:', error); + message.error('绑定过程中出现错误,请稍后重试'); + } finally { + setLoading(false); + } + }; + + const getBindTypeIcon = () => { + switch (bindType) { + case BindType.GitHub: + return ; + case BindType.LinuxDo: + return LinuxDo; + default: + return ; + } + }; + + const getBindTypeText = () => { + switch (bindType) { + case BindType.GitHub: + return 'GitHub'; + case BindType.LinuxDo: + return 'LinuxDo'; + default: + return '第三方'; + } + }; + + if (!thirdPartyUserId) { + return null; + } + + return ( + + + +
+
+ {getBindTypeIcon()} +
+ + 绑定{getBindTypeText()}账户 + + + 请输入您的Foxel账户信息来绑定{getBindTypeText()}账户 + +
+ + +

• 如果您已有Foxel账户,请输入邮箱和密码进行绑定

+

• 如果您还没有Foxel账户,系统将自动为您创建一个新账户

+

• 绑定后您可以使用{getBindTypeText()}账户快速登录

+ + } + type="info" + showIcon + style={{ marginBottom: '24px' }} + /> + +
+ + } + placeholder="请输入邮箱地址" + style={{ + height: '48px', + borderRadius: '8px' + }} + /> + + + + } + placeholder="请输入密码(6位以上)" + style={{ + height: '48px', + borderRadius: '8px' + }} + /> + + + + + + +
+ +
+
+
+ +
+ ); +}; + +export default Bind; diff --git a/Web/src/pages/login/Index.tsx b/Web/src/pages/login/Index.tsx index 7b6530b..47ecbd3 100644 --- a/Web/src/pages/login/Index.tsx +++ b/Web/src/pages/login/Index.tsx @@ -1,8 +1,8 @@ import React, {useState, useEffect} from 'react'; import {Form, Input, Button, Checkbox, Typography, Row, Col, Divider, message} from 'antd'; -import {UserOutlined, LockOutlined, GithubOutlined, GoogleOutlined} from '@ant-design/icons'; +import {UserOutlined, LockOutlined, GithubOutlined} from '@ant-design/icons'; import {useNavigate, Link} from 'react-router'; -import {login, saveAuthData, isAuthenticated, handleOAuthCallback, getGitHubLoginUrl} from '../../api'; +import {login, saveAuthData, isAuthenticated, handleOAuthCallback, getGitHubLoginUrl, getLinuxDoLoginUrl} from '../../api'; import useIsMobile from '../../hooks/useIsMobile'; const {Title, Text} = Typography; @@ -67,6 +67,10 @@ const Login: React.FC = () => { window.location.href = getGitHubLoginUrl(); }; + const handleLinuxDoLogin = () => { + window.location.href = getLinuxDoLoginUrl(); + }; + return ( {/* 左侧登录表单 */} @@ -184,9 +188,10 @@ const Login: React.FC = () => { }} />