mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat: implement account binding functionality for GitHub and LinuxDo
This commit is contained in:
+72
-6
@@ -9,7 +9,7 @@ using Foxel.Services.Configuration;
|
|||||||
namespace Foxel.Controllers;
|
namespace Foxel.Controllers;
|
||||||
|
|
||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController(IAuthService authService, IConfigService configService) : BaseApiController
|
public class AuthController(IAuthService authService) : BaseApiController
|
||||||
{
|
{
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<ActionResult<BaseResult<AuthResponse>>> Register([FromBody] RegisterRequest request)
|
public async Task<ActionResult<BaseResult<AuthResponse>>> Register([FromBody] RegisterRequest request)
|
||||||
@@ -108,14 +108,50 @@ public class AuthController(IAuthService authService, IConfigService configServi
|
|||||||
[HttpGet("github/callback")]
|
[HttpGet("github/callback")]
|
||||||
public async Task<ActionResult<BaseResult<string>>> GitHubCallback(string code)
|
public async Task<ActionResult<BaseResult<string>>> GitHubCallback(string code)
|
||||||
{
|
{
|
||||||
var (success, message, token) = await authService.ProcessGitHubCallbackAsync(code);
|
var (result, message, data) = await authService.ProcessGitHubCallbackAsync(code);
|
||||||
|
|
||||||
if (!success || token == null)
|
switch (result)
|
||||||
{
|
{
|
||||||
return Redirect($"/login?error=github_auth_failed&message={Uri.EscapeDataString(message)}");
|
case GitHubAuthResult.Success:
|
||||||
}
|
return Redirect($"/login?token={Uri.EscapeDataString(data!)}");
|
||||||
|
|
||||||
return Redirect($"/login?token={Uri.EscapeDataString(token)}");
|
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<ActionResult<BaseResult<string>>> 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)}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("update")]
|
[HttpPut("update")]
|
||||||
@@ -149,4 +185,34 @@ public class AuthController(IAuthService authService, IConfigService configServi
|
|||||||
|
|
||||||
return Success(profile, "用户信息更新成功");
|
return Success(profile, "用户信息更新成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("bind")]
|
||||||
|
public async Task<ActionResult<BaseResult<AuthResponse>>> BindAccount([FromBody] BindAccountRequest request)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return Error<AuthResponse>("请求数据无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, message, user) = await authService.BindAccountAsync(request);
|
||||||
|
if (!success || user == null)
|
||||||
|
{
|
||||||
|
return Error<AuthResponse>(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ public class User : BaseModel
|
|||||||
[Required] [StringLength(255)] public required string PasswordHash { get; set; }
|
[Required] [StringLength(255)] public required string PasswordHash { get; set; }
|
||||||
|
|
||||||
[StringLength(255)] public string? GithubId { get; set; }
|
[StringLength(255)] public string? GithubId { get; set; }
|
||||||
|
[StringLength(255)] public string? LinuxDoId { get; set; }
|
||||||
|
|
||||||
public int? RoleId { get; set; }
|
public int? RoleId { get; set; }
|
||||||
|
|
||||||
public Role? Role { get; set; }
|
public Role? Role { get; set; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+241
-41
@@ -27,6 +27,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
return (false, "该用户名已被使用", null);
|
return (false, "该用户名已被使用", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = new User
|
var user = new User
|
||||||
{
|
{
|
||||||
UserName = request.UserName,
|
UserName = request.UserName,
|
||||||
@@ -44,6 +45,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
user.RoleId = 1;
|
user.RoleId = 1;
|
||||||
user.Role = role;
|
user.Role = role;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Users.Add(user);
|
context.Users.Add(user);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
return (true, "用户注册成功", user);
|
return (true, "用户注册成功", user);
|
||||||
@@ -103,45 +105,34 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
return await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId);
|
return await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync(
|
public async Task<(bool success, string message, User? user)> FindGitHubUserAsync(string githubId)
|
||||||
string githubId, string? githubName, string? email)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
return (false, "GitHub账号未提供邮箱地址", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.GithubId == githubId);
|
||||||
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Email == email);
|
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
user = new User
|
return (false, "未找到对应的GitHub用户", null);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(user.GithubId))
|
return (true, "找到GitHub用户", user);
|
||||||
{
|
|
||||||
user.GithubId = githubId;
|
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId);
|
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
@@ -159,6 +150,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
return (false, "该用户名已被使用", null);
|
return (false, "该用户名已被使用", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.UserName = request.UserName;
|
user.UserName = request.UserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +162,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
return (false, "该邮箱已被注册", null);
|
return (false, "该邮箱已被注册", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Email = request.Email;
|
user.Email = request.Email;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,14 +192,15 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
string githubClientId = configuration["Authentication:GitHubClientId"];
|
string githubClientId = configuration["Authentication:GitHubClientId"];
|
||||||
string githubCallback = configuration["Authentication:GitHubCallbackUrl"];
|
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))
|
if (string.IsNullOrEmpty(code))
|
||||||
{
|
{
|
||||||
return (false, "GitHub授权码无效", null);
|
return (GitHubAuthResult.InvalidCode, "GitHub授权码无效", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
string githubClientId = configuration["Authentication:GitHubClientId"];
|
string githubClientId = configuration["Authentication:GitHubClientId"];
|
||||||
@@ -225,7 +219,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
||||||
return (false, $"获取GitHub访问令牌失败: {errorContent}", null);
|
return (GitHubAuthResult.TokenRequestFailed, $"获取GitHub访问令牌失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync();
|
var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
@@ -235,7 +229,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
accessTokenElement.GetString() == null)
|
accessTokenElement.GetString() == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"GitHub响应中未找到access_token: {tokenResponseContent}");
|
Console.WriteLine($"GitHub响应中未找到access_token: {tokenResponseContent}");
|
||||||
return (false, "获取GitHub访问令牌失败,响应中未包含令牌。", null);
|
return (GitHubAuthResult.TokenRequestFailed, "获取GitHub访问令牌失败,响应中未包含令牌。", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessToken = accessTokenElement.GetString();
|
var accessToken = accessTokenElement.GetString();
|
||||||
@@ -248,7 +242,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
{
|
{
|
||||||
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
||||||
return (false, $"获取GitHub用户信息失败: {errorContent}", null);
|
return (GitHubAuthResult.UserInfoFailed, $"获取GitHub用户信息失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userContent = await userResponse.Content.ReadAsStringAsync();
|
var userContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
@@ -281,18 +275,224 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(githubUserId))
|
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)
|
if (!isSuccess || user == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"创建或查找GitHub用户失败: {message}");
|
return (GitHubAuthResult.UserNotBound, "GitHub用户未绑定到系统账户", githubUserId);
|
||||||
return (false, message, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var jwtToken = await GenerateJwtTokenAsync(user);
|
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<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Foxel.Services.Auth;
|
||||||
|
|
||||||
|
public enum GitHubAuthResult
|
||||||
|
{
|
||||||
|
Success, // 授权成功并找到绑定用户
|
||||||
|
UserNotBound, // 授权成功但用户未绑定
|
||||||
|
InvalidCode, // 授权码无效
|
||||||
|
TokenRequestFailed, // 获取访问令牌失败
|
||||||
|
UserInfoFailed, // 获取用户信息失败
|
||||||
|
InvalidUserId // 无法获取GitHub用户ID
|
||||||
|
}
|
||||||
@@ -9,9 +9,12 @@ public interface IAuthService
|
|||||||
Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request);
|
Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request);
|
||||||
Task<string> GenerateJwtTokenAsync(User user);
|
Task<string> GenerateJwtTokenAsync(User user);
|
||||||
Task<User?> GetUserByIdAsync(int userId);
|
Task<User?> GetUserByIdAsync(int userId);
|
||||||
Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync(
|
Task<(bool success, string message, User? user)> FindGitHubUserAsync(string githubId);
|
||||||
string githubId, string? githubName, string? email);
|
Task<(bool success, string message, User? user)> FindLinuxDoUserAsync(string linuxdoId);
|
||||||
Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, UpdateUserRequest request);
|
Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, UpdateUserRequest request);
|
||||||
string GetGitHubLoginUrl();
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Foxel.Services.Auth;
|
||||||
|
|
||||||
|
public enum LinuxDoAuthResult
|
||||||
|
{
|
||||||
|
Success, // 授权成功并找到绑定用户
|
||||||
|
UserNotBound, // 授权成功但用户未绑定
|
||||||
|
InvalidCode, // 授权码无效
|
||||||
|
TokenRequestFailed, // 获取访问令牌失败
|
||||||
|
UserInfoFailed, // 获取用户信息失败
|
||||||
|
InvalidUserId // 无法获取LinuxDo用户ID
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg version="1.2" baseProfile="tiny-ps" width="200" height="200" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><title>LINUX DO Logo</title><clipPath id="a"><circle cx="60" cy="60" r="47"/></clipPath><circle fill="#f0f0f0" cx="60" cy="60" r="50"/><rect fill="#1c1c1e" clip-path="url(#a)" x="10" y="10" width="100" height="30"/><rect fill="#f0f0f0" clip-path="url(#a)" x="10" y="40" width="100" height="40"/><rect fill="#ffb003" clip-path="url(#a)" x="10" y="80" width="100" height="30"/></svg>
|
||||||
|
After Width: | Height: | Size: 542 B |
+2
-1
@@ -10,6 +10,7 @@ import { getMainRoutes, getAdminRoutes } from './routes';
|
|||||||
import { AuthProvider } from './auth/AuthContext';
|
import { AuthProvider } from './auth/AuthContext';
|
||||||
import AnonymousPage from './pages/anonymous/Index';
|
import AnonymousPage from './pages/anonymous/Index';
|
||||||
import AdminLayout from './layouts/AdminLayout';
|
import AdminLayout from './layouts/AdminLayout';
|
||||||
|
import Bind from './pages/bind/Index';
|
||||||
|
|
||||||
const PrivateRoute = ({ children }: { children: JSX.Element }) => {
|
const PrivateRoute = ({ children }: { children: JSX.Element }) => {
|
||||||
return isAuthenticated() ? children : <Navigate to="/login" />;
|
return isAuthenticated() ? children : <Navigate to="/login" />;
|
||||||
@@ -56,8 +57,8 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/bind" element={<Bind />} />
|
||||||
<Route path="/anonymous" element={<AnonymousPage />} />
|
<Route path="/anonymous" element={<AnonymousPage />} />
|
||||||
|
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<MainLayout />
|
<MainLayout />
|
||||||
|
|||||||
+22
-3
@@ -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';
|
import {fetchApi, BASE_URL} from './fetchClient';
|
||||||
|
|
||||||
// 认证数据本地存储键
|
// 认证数据本地存储键
|
||||||
@@ -89,6 +89,22 @@ export async function updateUserInfo(data: UpdateUserRequest): Promise<BaseResul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定账户
|
||||||
|
export async function bindAccount(data: BindAccountRequest): Promise<BaseResult<AuthResponse>> {
|
||||||
|
const response = await fetchApi<AuthResponse>('/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 => {
|
export const saveAuthData = (authData: AuthResponse): void => {
|
||||||
localStorage.setItem(TOKEN_KEY, authData.token);
|
localStorage.setItem(TOKEN_KEY, authData.token);
|
||||||
@@ -150,7 +166,6 @@ export async function handleOAuthCallback(): Promise<boolean> {
|
|||||||
|
|
||||||
saveAuthData(authResponse);
|
saveAuthData(authResponse);
|
||||||
|
|
||||||
// 清除URL中的token参数
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.delete('token');
|
url.searchParams.delete('token');
|
||||||
window.history.replaceState({}, document.title, url.toString());
|
window.history.replaceState({}, document.title, url.toString());
|
||||||
@@ -160,7 +175,7 @@ export async function handleOAuthCallback(): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('第三方登录处理失败:', error);
|
console.error('第三方登录处理失败:', error);
|
||||||
clearAuthData(); // 清除可能部分保存的数据
|
clearAuthData();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,3 +186,7 @@ export async function handleOAuthCallback(): Promise<boolean> {
|
|||||||
export function getGitHubLoginUrl(): string {
|
export function getGitHubLoginUrl(): string {
|
||||||
return `${BASE_URL}/auth/github/login`;
|
return `${BASE_URL}/auth/github/login`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLinuxDoLoginUrl(): string {
|
||||||
|
return `${BASE_URL}/auth/linuxdo/login`;
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ export {
|
|||||||
saveAuthData,
|
saveAuthData,
|
||||||
clearAuthData,
|
clearAuthData,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
getStoredUser
|
getStoredUser,
|
||||||
|
bindAccount,
|
||||||
|
getGitHubLoginUrl,
|
||||||
|
getLinuxDoLoginUrl
|
||||||
} from './authApi';
|
} from './authApi';
|
||||||
|
|
||||||
// 导出Picture API
|
// 导出Picture API
|
||||||
|
|||||||
@@ -271,3 +271,17 @@ export const VectorDbType = {
|
|||||||
export interface VectorDbInfo {
|
export interface VectorDbInfo {
|
||||||
type: string;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -251,6 +251,29 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
|
|||||||
</Form>
|
</Form>
|
||||||
</ConfigSection>
|
</ConfigSection>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
<TabPane tab="LinuxDo认证" key="linuxdo">
|
||||||
|
<ConfigSection
|
||||||
|
title="LinuxDo OAuth 配置"
|
||||||
|
icon={<GlobalOutlined />}
|
||||||
|
description="LinuxDo OAuth 应用配置,用于实现第三方登录功能"
|
||||||
|
isMobile={isMobile}
|
||||||
|
>
|
||||||
|
<Form form={formsMap.Authentication} layout="vertical" size={isMobile ? "middle" : "large"}>
|
||||||
|
{renderConfigFormItems(formsMap.Authentication, "Authentication", ["LinuxDoClientId", "LinuxDoClientSecret", "LinuxDoCallbackUrl"])}
|
||||||
|
<Divider style={{ margin: '12px 0 20px' }} />
|
||||||
|
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={() => onSaveAllForGroup(formsMap.Authentication, "Authentication", ["LinuxDoClientId", "LinuxDoClientSecret", "LinuxDoCallbackUrl"])}
|
||||||
|
style={{ width: isMobile ? '100%' : '240px' }}
|
||||||
|
>
|
||||||
|
保存所有 LinuxDo 认证配置
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ConfigSection>
|
||||||
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ const allDescriptions: Record<string, Record<string, string>> = {
|
|||||||
Authentication: {
|
Authentication: {
|
||||||
GitHubClientId: 'GitHub OAuth 应用客户端ID',
|
GitHubClientId: 'GitHub OAuth 应用客户端ID',
|
||||||
GitHubClientSecret: 'GitHub OAuth 应用客户端密钥',
|
GitHubClientSecret: 'GitHub OAuth 应用客户端密钥',
|
||||||
GitHubCallbackUrl: 'GitHub OAuth 认证回调地址'
|
GitHubCallbackUrl: 'GitHub OAuth 认证回调地址',
|
||||||
|
LinuxDoClientId: 'LinuxDo OAuth 应用客户端ID',
|
||||||
|
LinuxDoClientSecret: 'LinuxDo OAuth 应用客户端密钥',
|
||||||
|
LinuxDoCallbackUrl: 'LinuxDo OAuth 认证回调地址'
|
||||||
},
|
},
|
||||||
AppSettings: {
|
AppSettings: {
|
||||||
ServerUrl: '服务器URL'
|
ServerUrl: '服务器URL'
|
||||||
|
|||||||
@@ -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 <GithubOutlined style={{ fontSize: '24px', color: '#24292e' }} />;
|
||||||
|
case BindType.LinuxDo:
|
||||||
|
return <img src="/images/linuxdo.svg" alt="LinuxDo" style={{ width: '32px', height: '32px' }} />;
|
||||||
|
default:
|
||||||
|
return <LinkOutlined style={{ fontSize: '24px' }} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBindTypeText = () => {
|
||||||
|
switch (bindType) {
|
||||||
|
case BindType.GitHub:
|
||||||
|
return 'GitHub';
|
||||||
|
case BindType.LinuxDo:
|
||||||
|
return 'LinuxDo';
|
||||||
|
default:
|
||||||
|
return '第三方';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!thirdPartyUserId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row style={{ minHeight: '100vh', backgroundColor: '#f5f5f5', padding: isMobile ? '20px' : '40px' }}>
|
||||||
|
<Col
|
||||||
|
xs={24}
|
||||||
|
sm={20}
|
||||||
|
md={16}
|
||||||
|
lg={12}
|
||||||
|
xl={8}
|
||||||
|
style={{
|
||||||
|
margin: '0 auto',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '500px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: isMobile ? '24px' : '40px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
{getBindTypeIcon()}
|
||||||
|
</div>
|
||||||
|
<Title level={2} style={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: '#18181b'
|
||||||
|
}}>
|
||||||
|
绑定{getBindTypeText()}账户
|
||||||
|
</Title>
|
||||||
|
<Text style={{ fontSize: '16px', color: '#666' }}>
|
||||||
|
请输入您的Foxel账户信息来绑定{getBindTypeText()}账户
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
message="账户绑定说明"
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<p>• 如果您已有Foxel账户,请输入邮箱和密码进行绑定</p>
|
||||||
|
<p>• 如果您还没有Foxel账户,系统将自动为您创建一个新账户</p>
|
||||||
|
<p>• 绑定后您可以使用{getBindTypeText()}账户快速登录</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: '24px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
name="bind_form"
|
||||||
|
onFinish={onFinish}
|
||||||
|
size="large"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="邮箱"
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入您的邮箱' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
style={{
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="密码"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入您的密码' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6位' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
|
placeholder="请输入密码(6位以上)"
|
||||||
|
style={{
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: '16px' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '绑定中...' : `绑定${getBindTypeText()}账户`}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
style={{ padding: '0', color: '#666' }}
|
||||||
|
>
|
||||||
|
返回登录页面
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bind;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import {Form, Input, Button, Checkbox, Typography, Row, Col, Divider, message} from 'antd';
|
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 {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';
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
@@ -67,6 +67,10 @@ const Login: React.FC = () => {
|
|||||||
window.location.href = getGitHubLoginUrl();
|
window.location.href = getGitHubLoginUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinuxDoLogin = () => {
|
||||||
|
window.location.href = getLinuxDoLoginUrl();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={{height: '100vh', overflow: 'hidden'}}>
|
<Row style={{height: '100vh', overflow: 'hidden'}}>
|
||||||
{/* 左侧登录表单 */}
|
{/* 左侧登录表单 */}
|
||||||
@@ -184,9 +188,10 @@ const Login: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<GoogleOutlined/>}
|
icon={<img src="/images/linuxdo.svg" alt="LinuxDo" style={{width: '20px', height: '20px'}} />}
|
||||||
size="large"
|
size="large"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
|
onClick={handleLinuxDoLogin}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f6f6f6',
|
backgroundColor: '#f6f6f6',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Form, Input, Button, Checkbox, Typography, Row, Col, Divider, message } from 'antd';
|
import { Form, Input, Button, Checkbox, Typography, Row, Col, Divider, message } from 'antd';
|
||||||
import { UserOutlined, LockOutlined, MailOutlined, GithubOutlined, GoogleOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined, MailOutlined, GithubOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate, Link } from 'react-router';
|
import { useNavigate, Link } from 'react-router';
|
||||||
import { register, saveAuthData, isAuthenticated, handleOAuthCallback, getGitHubLoginUrl } from '../../api';
|
import { register, saveAuthData, isAuthenticated, handleOAuthCallback, getGitHubLoginUrl, getLinuxDoLoginUrl } from '../../api';
|
||||||
import useIsMobile from '../../hooks/useIsMobile';
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@@ -68,6 +68,10 @@ const Register: React.FC = () => {
|
|||||||
window.location.href = getGitHubLoginUrl();
|
window.location.href = getGitHubLoginUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinuxDoLogin = () => {
|
||||||
|
window.location.href = getLinuxDoLoginUrl();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={{ height: '100vh', overflow: 'hidden' }}>
|
<Row style={{ height: '100vh', overflow: 'hidden' }}>
|
||||||
{/* 左侧注册表单 */}
|
{/* 左侧注册表单 */}
|
||||||
@@ -245,9 +249,10 @@ const Register: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<GoogleOutlined />}
|
icon={<img src="/images/linuxdo.svg" alt="LinuxDo" style={{width: '20px', height: '20px'}} />}
|
||||||
size="large"
|
size="large"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
|
onClick={handleLinuxDoLogin}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f6f6f6',
|
backgroundColor: '#f6f6f6',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user