feat: implement account binding functionality for GitHub and LinuxDo

This commit is contained in:
shiyu
2025-06-01 15:39:14 +08:00
parent c458f3c6f7
commit d76bf5b751
17 changed files with 670 additions and 64 deletions
+241 -41
View File
@@ -27,6 +27,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
{
return (false, "该用户名已被使用", null);
}
var user = new User
{
UserName = request.UserName,
@@ -44,6 +45,7 @@ public class AuthService(IDbContextFactory<MyDbContext> 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<MyDbContext> 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<MyDbContext> dbContextFactory, IConfi
{
return (false, "该用户名已被使用", null);
}
user.UserName = request.UserName;
}
@@ -170,6 +162,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
{
return (false, "该邮箱已被注册", null);
}
user.Email = request.Email;
}
@@ -199,14 +192,15 @@ public class AuthService(IDbContextFactory<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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<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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace Foxel.Services.Auth;
public enum GitHubAuthResult
{
Success, // 授权成功并找到绑定用户
UserNotBound, // 授权成功但用户未绑定
InvalidCode, // 授权码无效
TokenRequestFailed, // 获取访问令牌失败
UserInfoFailed, // 获取用户信息失败
InvalidUserId // 无法获取GitHub用户ID
}
+6 -3
View File
@@ -9,9 +9,12 @@ public interface IAuthService
Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request);
Task<string> GenerateJwtTokenAsync(User user);
Task<User?> 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);
}
+11
View File
@@ -0,0 +1,11 @@
namespace Foxel.Services.Auth;
public enum LinuxDoAuthResult
{
Success, // 授权成功并找到绑定用户
UserNotBound, // 授权成功但用户未绑定
InvalidCode, // 授权码无效
TokenRequestFailed, // 获取访问令牌失败
UserInfoFailed, // 获取用户信息失败
InvalidUserId // 无法获取LinuxDo用户ID
}