mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-14 00:41:38 +08:00
feat(auth): add user information update feature and refactor the authentication service.
This commit is contained in:
@@ -8,7 +8,7 @@ using Foxel.Models.Response.Auth;
|
|||||||
namespace Foxel.Controllers;
|
namespace Foxel.Controllers;
|
||||||
|
|
||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController(IUserService userService, IConfigService configService) : BaseApiController
|
public class AuthController(IAuthService authService, IConfigService configService) : 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)
|
||||||
@@ -18,13 +18,13 @@ public class AuthController(IUserService userService, IConfigService configServi
|
|||||||
return Error<AuthResponse>("请求数据无效");
|
return Error<AuthResponse>("请求数据无效");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (success, message, user) = await userService.RegisterUserAsync(request);
|
var (success, message, user) = await authService.RegisterUserAsync(request);
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
return Error<AuthResponse>(message);
|
return Error<AuthResponse>(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = await userService.GenerateJwtTokenAsync(user!);
|
var token = await authService.GenerateJwtTokenAsync(user!);
|
||||||
var response = new AuthResponse
|
var response = new AuthResponse
|
||||||
{
|
{
|
||||||
Token = token,
|
Token = token,
|
||||||
@@ -48,13 +48,13 @@ public class AuthController(IUserService userService, IConfigService configServi
|
|||||||
return Error<AuthResponse>("请求数据无效");
|
return Error<AuthResponse>("请求数据无效");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (success, message, user) = await userService.AuthenticateUserAsync(request);
|
var (success, message, user) = await authService.AuthenticateUserAsync(request);
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
return Error<AuthResponse>(message, 401);
|
return Error<AuthResponse>(message, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = await userService.GenerateJwtTokenAsync(user!);
|
var token = await authService.GenerateJwtTokenAsync(user!);
|
||||||
var response = new AuthResponse
|
var response = new AuthResponse
|
||||||
{
|
{
|
||||||
Token = token,
|
Token = token,
|
||||||
@@ -80,7 +80,7 @@ public class AuthController(IUserService userService, IConfigService configServi
|
|||||||
return Error<UserProfile>("用户ID未找到");
|
return Error<UserProfile>("用户ID未找到");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await userService.GetUserByIdAsync(userId.Value);
|
var user = await authService.GetUserByIdAsync(userId.Value);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return Error<UserProfile>("未找到用户信息", 404);
|
return Error<UserProfile>("未找到用户信息", 404);
|
||||||
@@ -100,108 +100,52 @@ public class AuthController(IUserService userService, IConfigService configServi
|
|||||||
[HttpGet("github/login")]
|
[HttpGet("github/login")]
|
||||||
public IActionResult GitHubLogin()
|
public IActionResult GitHubLogin()
|
||||||
{
|
{
|
||||||
string githubClientId = configService["Authentication:GitHubClientId"];
|
string githubAuthorizeUrl = authService.GetGitHubLoginUrl();
|
||||||
string githubCallback = configService["Authentication:GitHubCallbackUrl"];
|
|
||||||
string githubAuthorizeUrl =
|
|
||||||
$"https://github.com/login/oauth/authorize?client_id={Uri.EscapeDataString(githubClientId)}&redirect_uri={Uri.EscapeDataString(githubCallback)}";
|
|
||||||
return Redirect(githubAuthorizeUrl);
|
return Redirect(githubAuthorizeUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("github/callback")]
|
[HttpGet("github/callback")]
|
||||||
public async Task<ActionResult<BaseResult<string>>> GitHubCallback(string code)
|
public async Task<ActionResult<BaseResult<string>>> GitHubCallback(string code)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(code))
|
var (success, message, token) = await authService.ProcessGitHubCallbackAsync(code);
|
||||||
|
|
||||||
|
if (!success || token == null)
|
||||||
{
|
{
|
||||||
return Error<string>("GitHub授权码无效");
|
return Redirect($"/login?error=github_auth_failed&message={Uri.EscapeDataString(message)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
string githubClientId = configService["Authentication:GitHubClientId"];
|
|
||||||
string githubClientSecret = configService["Authentication:GitHubClientSecret"];
|
|
||||||
string githubTokenUrl = "https://github.com/login/oauth/access_token";
|
|
||||||
string githubUserApiUrl = "https://api.github.com/user";
|
|
||||||
|
|
||||||
using var httpClient = new HttpClient();
|
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", "Foxel");
|
|
||||||
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
|
||||||
var tokenRequestUrl =
|
|
||||||
$"{githubTokenUrl}?client_id={Uri.EscapeDataString(githubClientId)}&client_secret={Uri.EscapeDataString(githubClientSecret)}&code={Uri.EscapeDataString(code)}";
|
|
||||||
var tokenResponse = await httpClient.PostAsync(tokenRequestUrl, null);
|
|
||||||
|
|
||||||
if (!tokenResponse.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
|
||||||
Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
|
||||||
return Error<string>($"获取GitHub访问令牌失败: {errorContent}", (int)tokenResponse.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
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($"GitHub响应中未找到access_token: {tokenResponseContent}");
|
|
||||||
return Error<string>("获取GitHub访问令牌失败,响应中未包含令牌。");
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessToken = accessTokenElement.GetString();
|
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.Authorization =
|
|
||||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
|
||||||
|
|
||||||
var userResponse = await httpClient.GetAsync(githubUserApiUrl);
|
|
||||||
if (!userResponse.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
|
||||||
Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
|
||||||
return Error<string>($"获取GitHub用户信息失败: {errorContent}", (int)userResponse.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userContent = await userResponse.Content.ReadAsStringAsync();
|
|
||||||
var userJson = System.Text.Json.JsonDocument.Parse(userContent);
|
|
||||||
|
|
||||||
string? githubUserId = null;
|
|
||||||
string? email = null;
|
|
||||||
string? name = null;
|
|
||||||
string? loginName = null;
|
|
||||||
|
|
||||||
if (userJson.RootElement.TryGetProperty("id", out var idElement))
|
|
||||||
{
|
|
||||||
githubUserId = idElement.GetInt64().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userJson.RootElement.TryGetProperty("email", out var emailElement))
|
|
||||||
{
|
|
||||||
email = emailElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userJson.RootElement.TryGetProperty("name", out var nameElement))
|
|
||||||
{
|
|
||||||
name = nameElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userJson.RootElement.TryGetProperty("login", out var loginElement))
|
|
||||||
{
|
|
||||||
loginName = loginElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(githubUserId))
|
|
||||||
{
|
|
||||||
return Error<string>("无法从GitHub获取用户ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var (isSuccess, message, user) =
|
|
||||||
await userService.FindOrCreateGitHubUserAsync(githubUserId, name ?? loginName, email);
|
|
||||||
|
|
||||||
if (!isSuccess || user == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"创建或查找GitHub用户失败: {message}");
|
|
||||||
return Redirect(
|
|
||||||
$"/login?error=github_user_creation_failed&message={Uri.EscapeDataString(message)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = await userService.GenerateJwtTokenAsync(user);
|
|
||||||
return Redirect($"/login?token={Uri.EscapeDataString(token)}");
|
return Redirect($"/login?token={Uri.EscapeDataString(token)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("update")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<BaseResult<UserProfile>>> UpdateUserInfo([FromBody] UpdateUserRequest request)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return Error<UserProfile>("请求数据无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Error<UserProfile>("用户ID未找到");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, message, user) = await authService.UpdateUserInfoAsync(userId.Value, request);
|
||||||
|
if (!success || user == null)
|
||||||
|
{
|
||||||
|
return Error<UserProfile>(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile = new UserProfile
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Email = user.Email,
|
||||||
|
UserName = user.UserName,
|
||||||
|
RoleName = user.Role?.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
return Success(profile, "用户信息更新成功");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<IConfigService, ConfigService>();
|
services.AddSingleton<IConfigService, ConfigService>();
|
||||||
services.AddSingleton<IAiService, AiService>();
|
services.AddSingleton<IAiService, AiService>();
|
||||||
services.AddSingleton<IPictureService, PictureService>();
|
services.AddSingleton<IPictureService, PictureService>();
|
||||||
services.AddSingleton<IUserService, UserService>();
|
services.AddSingleton<IAuthService, AuthService>();
|
||||||
services.AddSingleton<ITagService, TagService>();
|
services.AddSingleton<ITagService, TagService>();
|
||||||
services.AddSingleton<IAlbumService, AlbumService>();
|
services.AddSingleton<IAlbumService, AlbumService>();
|
||||||
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||||
|
|||||||
290
Services/AuthService.cs
Normal file
290
Services/AuthService.cs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Foxel.Models.DataBase;
|
||||||
|
using Foxel.Services.Interface;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Foxel.Models.Request.Auth;
|
||||||
|
using static Foxel.Utils.AuthHelper;
|
||||||
|
|
||||||
|
namespace Foxel.Services;
|
||||||
|
|
||||||
|
public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfigService configuration)
|
||||||
|
: IAuthService
|
||||||
|
{
|
||||||
|
public async Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request)
|
||||||
|
{
|
||||||
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
var existingUser = await context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
return (false, "该邮箱已被注册", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUser = await context.Users.FirstOrDefaultAsync(u => u.UserName == request.UserName);
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
return (false, "该用户名已被使用", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
UserName = request.UserName,
|
||||||
|
Email = request.Email,
|
||||||
|
PasswordHash = HashPassword(request.Password),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
context.Users.Add(user);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return (true, "用户注册成功", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request)
|
||||||
|
{
|
||||||
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Email == request.Email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return (false, "用户不存在", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VerifyPassword(request.Password, user.PasswordHash))
|
||||||
|
{
|
||||||
|
return (false, "密码错误", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, "登录成功", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GenerateJwtTokenAsync(User user)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new(ClaimTypes.Email, user.Email),
|
||||||
|
new(ClaimTypes.Name, user.UserName)
|
||||||
|
};
|
||||||
|
if (user.Role != null)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, user.Role.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"] ??
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"JWT Secret key not found")));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
var expires = DateTime.UtcNow.AddYears(1);
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: configuration["Jwt:Issuer"],
|
||||||
|
audience: configuration["Jwt:Audience"],
|
||||||
|
claims: claims,
|
||||||
|
expires: expires,
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
return Task.FromResult(tokenString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserByIdAsync(int userId)
|
||||||
|
{
|
||||||
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(user.GithubId))
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return (false, "用户不存在", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
if (!string.IsNullOrEmpty(request.UserName) && request.UserName != user.UserName)
|
||||||
|
{
|
||||||
|
var existingUserName = await context.Users.AnyAsync(u => u.UserName == request.UserName);
|
||||||
|
if (existingUserName)
|
||||||
|
{
|
||||||
|
return (false, "该用户名已被使用", null);
|
||||||
|
}
|
||||||
|
user.UserName = request.UserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
if (!string.IsNullOrEmpty(request.Email) && request.Email != user.Email)
|
||||||
|
{
|
||||||
|
var existingEmail = await context.Users.AnyAsync(u => u.Email == request.Email);
|
||||||
|
if (existingEmail)
|
||||||
|
{
|
||||||
|
return (false, "该邮箱已被注册", null);
|
||||||
|
}
|
||||||
|
user.Email = request.Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果要修改密码,验证当前密码
|
||||||
|
if (!string.IsNullOrEmpty(request.NewPassword))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.CurrentPassword))
|
||||||
|
{
|
||||||
|
return (false, "需要提供当前密码", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VerifyPassword(request.CurrentPassword, user.PasswordHash))
|
||||||
|
{
|
||||||
|
return (false, "当前密码不正确", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = HashPassword(request.NewPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return (true, "用户信息更新成功", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetGitHubLoginUrl()
|
||||||
|
{
|
||||||
|
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)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool success, string message, string? token)> ProcessGitHubCallbackAsync(string code)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(code))
|
||||||
|
{
|
||||||
|
return (false, "GitHub授权码无效", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
string githubClientId = configuration["Authentication:GitHubClientId"];
|
||||||
|
string githubClientSecret = configuration["Authentication:GitHubClientSecret"];
|
||||||
|
string githubTokenUrl = "https://github.com/login/oauth/access_token";
|
||||||
|
string githubUserApiUrl = "https://api.github.com/user";
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Add("User-Agent", "Foxel");
|
||||||
|
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
var tokenRequestUrl =
|
||||||
|
$"{githubTokenUrl}?client_id={Uri.EscapeDataString(githubClientId)}&client_secret={Uri.EscapeDataString(githubClientSecret)}&code={Uri.EscapeDataString(code)}";
|
||||||
|
var tokenResponse = await httpClient.PostAsync(tokenRequestUrl, null);
|
||||||
|
|
||||||
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
||||||
|
return (false, $"获取GitHub访问令牌失败: {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($"GitHub响应中未找到access_token: {tokenResponseContent}");
|
||||||
|
return (false, "获取GitHub访问令牌失败,响应中未包含令牌。", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessToken = accessTokenElement.GetString();
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
|
|
||||||
|
var userResponse = await httpClient.GetAsync(githubUserApiUrl);
|
||||||
|
if (!userResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
||||||
|
return (false, $"获取GitHub用户信息失败: {errorContent}", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
|
var userJson = System.Text.Json.JsonDocument.Parse(userContent);
|
||||||
|
|
||||||
|
string? githubUserId = null;
|
||||||
|
string? email = null;
|
||||||
|
string? name = null;
|
||||||
|
string? loginName = null;
|
||||||
|
|
||||||
|
if (userJson.RootElement.TryGetProperty("id", out var idElement))
|
||||||
|
{
|
||||||
|
githubUserId = idElement.GetInt64().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJson.RootElement.TryGetProperty("email", out var emailElement))
|
||||||
|
{
|
||||||
|
email = emailElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJson.RootElement.TryGetProperty("name", out var nameElement))
|
||||||
|
{
|
||||||
|
name = nameElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJson.RootElement.TryGetProperty("login", out var loginElement))
|
||||||
|
{
|
||||||
|
loginName = loginElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(githubUserId))
|
||||||
|
{
|
||||||
|
return (false, "无法从GitHub获取用户ID", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (isSuccess, message, user) = await FindOrCreateGitHubUserAsync(githubUserId, name ?? loginName, email);
|
||||||
|
|
||||||
|
if (!isSuccess || user == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"创建或查找GitHub用户失败: {message}");
|
||||||
|
return (false, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwtToken = await GenerateJwtTokenAsync(user);
|
||||||
|
return (true, "GitHub授权成功", jwtToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using Foxel.Models.Request.Auth;
|
|||||||
|
|
||||||
namespace Foxel.Services.Interface;
|
namespace Foxel.Services.Interface;
|
||||||
|
|
||||||
public interface IUserService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request);
|
Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request);
|
||||||
Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request);
|
Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request);
|
||||||
@@ -12,4 +12,7 @@ public interface IUserService
|
|||||||
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)> FindOrCreateGitHubUserAsync(
|
||||||
string githubId, string? githubName, string? email);
|
string githubId, string? githubName, string? email);
|
||||||
|
Task<(bool success, string message, User? user)> UpdateUserInfoAsync(int userId, UpdateUserRequest request);
|
||||||
|
string GetGitHubLoginUrl();
|
||||||
|
Task<(bool success, string message, string? token)> ProcessGitHubCallbackAsync(string code);
|
||||||
}
|
}
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using Foxel.Models.DataBase;
|
|
||||||
using Foxel.Services.Interface;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Foxel.Models.Request;
|
|
||||||
using Foxel.Models.Request.Auth;
|
|
||||||
using static Foxel.Utils.AuthHelper;
|
|
||||||
|
|
||||||
namespace Foxel.Services;
|
|
||||||
|
|
||||||
public class UserService(IDbContextFactory<MyDbContext> dbContextFactory, IConfigService configuration)
|
|
||||||
: IUserService
|
|
||||||
{
|
|
||||||
public async Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request)
|
|
||||||
{
|
|
||||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
|
||||||
var existingUser = await context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
|
||||||
if (existingUser != null)
|
|
||||||
{
|
|
||||||
return (false, "该邮箱已被注册", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUser = await context.Users.FirstOrDefaultAsync(u => u.UserName == request.UserName);
|
|
||||||
if (existingUser != null)
|
|
||||||
{
|
|
||||||
return (false, "该用户名已被使用", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = new User
|
|
||||||
{
|
|
||||||
UserName = request.UserName,
|
|
||||||
Email = request.Email,
|
|
||||||
PasswordHash = HashPassword(request.Password),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
UpdatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
context.Users.Add(user);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
return (true, "用户注册成功", user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool success, string message, User? user)> AuthenticateUserAsync(LoginRequest request)
|
|
||||||
{
|
|
||||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
|
||||||
var user = await context.Users.Include(x => x.Role).FirstOrDefaultAsync(u => u.Email == request.Email);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return (false, "用户不存在", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!VerifyPassword(request.Password, user.PasswordHash))
|
|
||||||
{
|
|
||||||
return (false, "密码错误", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, "登录成功", user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> GenerateJwtTokenAsync(User user)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
||||||
new(ClaimTypes.Email, user.Email),
|
|
||||||
new(ClaimTypes.Name, user.UserName)
|
|
||||||
};
|
|
||||||
if (user.Role != null)
|
|
||||||
{
|
|
||||||
claims.Add(new Claim(ClaimTypes.Role, user.Role.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"] ??
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"JWT Secret key not found")));
|
|
||||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
||||||
var expires = DateTime.UtcNow.AddYears(1);
|
|
||||||
var token = new JwtSecurityToken(
|
|
||||||
issuer: configuration["Jwt:Issuer"],
|
|
||||||
audience: configuration["Jwt:Audience"],
|
|
||||||
claims: claims,
|
|
||||||
expires: expires,
|
|
||||||
signingCredentials: creds
|
|
||||||
);
|
|
||||||
|
|
||||||
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
|
|
||||||
return Task.FromResult(tokenString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> GetUserByIdAsync(int userId)
|
|
||||||
{
|
|
||||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(user.GithubId))
|
|
||||||
{
|
|
||||||
user.GithubId = githubId;
|
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, "GitHub用户登录成功", user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile} from './types';
|
import {type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile, type UpdateUserRequest} from './types';
|
||||||
import {fetchApi, BASE_URL} from './fetchClient';
|
import {fetchApi, BASE_URL} from './fetchClient';
|
||||||
|
|
||||||
// 认证数据本地存储键
|
// 认证数据本地存储键
|
||||||
@@ -59,6 +59,36 @@ export async function getCurrentUser(): Promise<BaseResult<UserProfile>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
export async function updateUserInfo(data: UpdateUserRequest): Promise<BaseResult<UserProfile>> {
|
||||||
|
try {
|
||||||
|
const response = await fetchApi<UserProfile>('/auth/update', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果成功更新用户数据,更新本地存储
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const user = getStoredUser();
|
||||||
|
if (user) {
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
...response.data
|
||||||
|
};
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(updatedUser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `更新用户信息失败: ${error.message}`,
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存认证数据到本地存储
|
// 保存认证数据到本地存储
|
||||||
export const saveAuthData = (authData: AuthResponse): void => {
|
export const saveAuthData = (authData: AuthResponse): void => {
|
||||||
localStorage.setItem(TOKEN_KEY, authData.token);
|
localStorage.setItem(TOKEN_KEY, authData.token);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export {
|
|||||||
register,
|
register,
|
||||||
login,
|
login,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
updateUserInfo, // 添加导出更新用户信息函数
|
||||||
saveAuthData,
|
saveAuthData,
|
||||||
clearAuthData,
|
clearAuthData,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
|||||||
@@ -198,3 +198,10 @@ export const UserRole = {
|
|||||||
User: "User" as UserRole,
|
User: "User" as UserRole,
|
||||||
Guest: "" as UserRole
|
Guest: "" as UserRole
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
userName?: string;
|
||||||
|
email?: string;
|
||||||
|
currentPassword?: string;
|
||||||
|
newPassword?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,70 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, Form, Input, Button } from 'antd';
|
import { Card, Form, Input, Button, message } from 'antd';
|
||||||
import { useAuth } from '../../api/AuthContext';
|
import { useAuth } from '../../api/AuthContext';
|
||||||
import UserAvatar from '../../components/UserAvatar';
|
import UserAvatar from '../../components/UserAvatar';
|
||||||
import useIsMobile from '../../hooks/useIsMobile';
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
import { updateUserInfo } from '../../api';
|
||||||
|
|
||||||
const UserProfile: React.FC = () => {
|
const UserProfile: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user, refreshUser } = useAuth();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
// 验证两次密码输入是否一致
|
||||||
|
if (values.newPassword && values.newPassword !== values.confirmPassword) {
|
||||||
|
message.error('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateData = {
|
||||||
|
userName: values.username !== user?.userName ? values.username : undefined,
|
||||||
|
email: values.email !== user?.email ? values.email : undefined,
|
||||||
|
currentPassword: values.currentPassword,
|
||||||
|
newPassword: values.newPassword
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤掉空值
|
||||||
|
Object.keys(updateData).forEach(key => {
|
||||||
|
if (updateData[key as keyof typeof updateData] === undefined ||
|
||||||
|
updateData[key as keyof typeof updateData] === '') {
|
||||||
|
delete updateData[key as keyof typeof updateData];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有需要更新的内容,直接返回
|
||||||
|
if (Object.keys(updateData).length === 0) {
|
||||||
|
message.info('没有内容需要更新');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await updateUserInfo(updateData);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
message.success('个人信息更新成功');
|
||||||
|
// 更新Context中的用户信息
|
||||||
|
refreshUser();
|
||||||
|
// 清除密码字段
|
||||||
|
form.setFieldsValue({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '更新失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新个人信息时出错:', error);
|
||||||
|
message.error('系统错误,请稍后再试');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -33,14 +91,23 @@ const UserProfile: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
username: user?.userName || '',
|
username: user?.userName || '',
|
||||||
email: user?.email || '',
|
email: user?.email || '',
|
||||||
}}
|
}}
|
||||||
size={isMobile ? "middle" : "large"}
|
size={isMobile ? "middle" : "large"}
|
||||||
|
onFinish={handleSubmit}
|
||||||
>
|
>
|
||||||
<Form.Item name="username" label="用户名">
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ max: 50, message: '用户名最长50个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Input placeholder="用户名" />
|
<Input placeholder="用户名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -48,19 +115,57 @@ const UserProfile: React.FC = () => {
|
|||||||
<Input placeholder="邮箱" disabled />
|
<Input placeholder="邮箱" disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="password" label="更新密码">
|
<Form.Item
|
||||||
<Input.Password placeholder="留空则不更改" />
|
name="currentPassword"
|
||||||
|
label="当前密码"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
message: '更改密码时需要输入当前密码'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltip="修改密码时必填,其他情况可不填"
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="只有修改密码时才需要填写" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="confirmPassword" label="确认新密码">
|
<Form.Item
|
||||||
|
name="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
rules={[
|
||||||
|
{ min: 6, message: '密码至少6位' },
|
||||||
|
{ max: 20, message: '密码最长20位' }
|
||||||
|
]}
|
||||||
|
dependencies={['currentPassword']}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="留空则不更改密码" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="confirmPassword"
|
||||||
|
label="确认新密码"
|
||||||
|
dependencies={['newPassword']}
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('newPassword') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Input.Password placeholder="确认新密码" />
|
<Input.Password placeholder="确认新密码" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
block={isMobile}
|
block={isMobile}
|
||||||
size={isMobile ? "middle" : "large"}
|
size={isMobile ? "middle" : "large"}
|
||||||
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
height: isMobile ? 40 : 'auto'
|
height: isMobile ? 40 : 'auto'
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user