refactor: improve GitHub OAuth handling and update authentication flow

This commit is contained in:
shiyu
2025-05-19 12:16:53 +08:00
parent b65f1e4bbb
commit ab83c519c4
12 changed files with 488 additions and 454 deletions
+97 -63
View File
@@ -1,17 +1,14 @@
using System.Security.Claims;
using Foxel.Models; using Foxel.Models;
using Foxel.Services.Interface; using Foxel.Services.Interface;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Foxel.Models.Request.Auth; using Foxel.Models.Request.Auth;
using Foxel.Models.Response.Auth; using Foxel.Models.Response.Auth;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace Foxel.Controllers; namespace Foxel.Controllers;
[Route("api/auth")] [Route("api/auth")]
public class AuthController(IUserService userService) : BaseApiController public class AuthController(IUserService userService, 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)
@@ -101,73 +98,110 @@ public class AuthController(IUserService userService) : BaseApiController
} }
[HttpGet("github/login")] [HttpGet("github/login")]
public IActionResult GitHubLogin(string returnUrl = "/") public IActionResult GitHubLogin()
{ {
try string githubClientId = configService["Authentication:GitHubClientId"];
{ string githubCallback = configService["Authentication:GitHubRedirectUri"];
var properties = new AuthenticationProperties string githubAuthorizeUrl =
{ $"https://github.com/login/oauth/authorize?client_id={Uri.EscapeDataString(githubClientId)}&redirect_uri={Uri.EscapeDataString(githubCallback)}";
RedirectUri = Url.Action("GitHubCallback", new { returnUrl }), return Redirect(githubAuthorizeUrl);
Items = { { "returnUrl", returnUrl } },
// 添加超时设置
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
IsPersistent = false
};
return Challenge(properties, "GitHub");
}
catch (Exception ex)
{
Console.WriteLine($"GitHub登录异常: {ex}");
return Redirect("/login?error=github_login_error");
}
} }
[HttpGet("github/callback")] [HttpGet("github/callback")]
public async Task<IActionResult> GitHubCallback(string returnUrl = "/") public async Task<ActionResult<BaseResult<string>>> GitHubCallback(string code)
{ {
try if (string.IsNullOrEmpty(code))
{ {
var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Error<string>("GitHub授权码无效");
if (!authenticateResult.Succeeded)
{
Console.WriteLine("GitHub认证失败: 无法获取认证结果");
return Redirect("/login?error=github_auth_failed");
}
// 获取GitHub用户信息
var githubId = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var githubLogin = authenticateResult.Principal.FindFirst("urn:github:login")?.Value;
var githubEmail = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value;
Console.WriteLine($"GitHub用户信息: ID={githubId}, Login={githubLogin}, Email={githubEmail}");
if (string.IsNullOrEmpty(githubId) || string.IsNullOrEmpty(githubLogin))
{
return Redirect("/login?error=github_missing_info");
}
// 登出Cookie认证会话
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// 查找或创建用户
var (success, message, user) = await userService.FindOrCreateGitHubUserAsync(
githubId, githubLogin, githubEmail);
if (!success || user == null)
{
return Redirect($"/login?error={Uri.EscapeDataString(message)}");
}
// 生成JWT令牌
var token = await userService.GenerateJwtTokenAsync(user);
// 重定向回前端,携带token参数
return Redirect($"{returnUrl}?token={Uri.EscapeDataString(token)}");
} }
catch (Exception ex)
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)
{ {
Console.WriteLine($"GitHub回调处理异常: {ex}"); var errorContent = await tokenResponse.Content.ReadAsStringAsync();
return Redirect("/login?error=github_callback_error"); 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)}");
} }
} }
+3 -2
View File
@@ -1,6 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081
EXPOSE 80 EXPOSE 80
FROM oven/bun:alpine AS build-frontend FROM oven/bun:alpine AS build-frontend
@@ -31,8 +32,8 @@ RUN apt-get update && apt-get install -y nginx && rm -rf /var/lib/apt/lists/*
COPY --from=build-frontend /src/View/dist /var/www/html COPY --from=build-frontend /src/View/dist /var/www/html
COPY /View/nginx.conf /etc/nginx/nginx.conf COPY /View/nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p /var/lib/nginx/body /var/cache/nginx /var/run/nginx \ RUN mkdir -p /var/lib/nginx/body /var/cache/nginx /var/run/nginx /app/Uploads \
&& chown -R $APP_UID:$APP_UID /var/lib/nginx /var/cache/nginx /var/run/nginx /var/log/nginx /etc/nginx /var/www/html \ && chown -R $APP_UID:$APP_UID /var/lib/nginx /var/cache/nginx /var/run/nginx /var/log/nginx /etc/nginx /var/www/html /app/Uploads \
&& mkdir -p /run \ && mkdir -p /run \
&& chmod 777 /run && chmod 777 /run
-29
View File
@@ -67,35 +67,6 @@ public static class ServiceCollectionExtensions
IssuerSigningKey = new SymmetricSecurityKey( IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])) Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"]))
}; };
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
})
.AddGitHub(options =>
{
options.ClientId = configuration["Authentication:GitHubClientId"];
options.ClientSecret = configuration["Authentication:GitHubClientSecret"];
options.CallbackPath = "/api/auth/github/callback";
options.Scope.Add("user:email");
options.BackchannelHttpHandler = new HttpClientHandler
{
UseCookies = false,
AllowAutoRedirect = false,
MaxConnectionsPerServer = 100
};
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents
{
OnRemoteFailure = context =>
{
Console.WriteLine($"GitHub登录失败: {context.Failure}");
context.Response.Redirect("/login?error=github_remote_error");
context.HandleResponse();
return Task.CompletedTask;
}
};
}); });
} }
+1 -1
View File
@@ -4,10 +4,10 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="9.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
+9 -4
View File
@@ -1,5 +1,6 @@
using Foxel.Extensions; using Foxel.Extensions;
using Foxel.Services.Interface; using Foxel.Services.Interface;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var environment = builder.Environment; var environment = builder.Environment;
@@ -14,26 +15,30 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddApplicationAuthentication(); builder.Services.AddApplicationAuthentication();
builder.Services.AddApplicationAuthorization(); builder.Services.AddApplicationAuthorization();
builder.Services.AddApplicationCors(); builder.Services.AddApplicationCors();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build(); var app = builder.Build();
// 初始化数据库配置
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var initializer = scope.ServiceProvider.GetRequiredService<IDatabaseInitializer>(); var initializer = scope.ServiceProvider.GetRequiredService<IDatabaseInitializer>();
await initializer.InitializeAsync(); await initializer.InitializeAsync();
} }
app.UseForwardedHeaders();
app.UseApplicationStaticFiles(); app.UseApplicationStaticFiles();
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts(); app.UseHsts();
} }
app.UseHttpsRedirection();
app.UseApplicationOpenApi(); app.UseApplicationOpenApi();
app.UseCors("MyAllowSpecificOrigins"); app.UseCors("MyAllowSpecificOrigins");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.UseHttpsRedirection();
app.Run(); app.Run();
+1 -1
View File
@@ -11,5 +11,5 @@ public interface IUserService
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)> FindOrCreateGitHubUserAsync(
string githubId, string githubLogin, string? email); string githubId, string? githubName, string? email);
} }
+10 -13
View File
@@ -97,7 +97,7 @@ public class UserService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
} }
public async Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync( public async Task<(bool success, string message, User? user)> FindOrCreateGitHubUserAsync(
string githubId, string githubLogin, string? email) string githubId, string? githubName, string? email)
{ {
if (string.IsNullOrEmpty(email)) if (string.IsNullOrEmpty(email))
{ {
@@ -106,17 +106,15 @@ public class UserService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
await using var context = await dbContextFactory.CreateDbContextAsync(); await using var context = await dbContextFactory.CreateDbContextAsync();
// 先尝试通过邮箱查找用户
var user = await context.Users.FirstOrDefaultAsync(u => u.Email == email); var user = await context.Users.FirstOrDefaultAsync(u => u.Email == email);
if (user == null) if (user == null)
{ {
// 用户不存在,创建新用户
user = new User user = new User
{ {
UserName = $"{githubLogin}_{githubId.Substring(0, 5)}", // 创建唯一用户名 UserName = $"{githubName}",
Email = email, Email = email,
PasswordHash = HashPassword(Guid.NewGuid().ToString()), PasswordHash = HashPassword(Guid.NewGuid().ToString()),
GithubId = githubId, GithubId = githubId,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow
@@ -125,15 +123,14 @@ public class UserService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
await context.SaveChangesAsync(); await context.SaveChangesAsync();
return (true, "GitHub用户注册成功", user); return (true, "GitHub用户注册成功", user);
} }
else
if (string.IsNullOrEmpty(user.GithubId))
{ {
if (string.IsNullOrEmpty(user.GithubId)) user.GithubId = githubId;
{ user.UpdatedAt = DateTime.UtcNow;
user.GithubId = githubId; await context.SaveChangesAsync();
user.UpdatedAt = DateTime.UtcNow;
await context.SaveChangesAsync();
}
return (true, "GitHub用户登录成功", user);
} }
return (true, "GitHub用户登录成功", user);
} }
} }
+2
View File
@@ -24,6 +24,7 @@ http {
proxy_set_header Connection keep-alive; proxy_set_header Connection keep-alive;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_ssl_verify off;
} }
location /uploads { location /uploads {
@@ -33,6 +34,7 @@ http {
proxy_set_header Connection keep-alive; proxy_set_header Connection keep-alive;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_ssl_verify off;
} }
} }
} }
+33 -45
View File
@@ -1,5 +1,5 @@
import { type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile } from './types'; import {type BaseResult, type AuthResponse, type LoginRequest, type RegisterRequest, type UserProfile} from './types';
import { fetchApi, BASE_URL } from './fetchClient'; import {fetchApi, BASE_URL} from './fetchClient';
// 认证数据本地存储键 // 认证数据本地存储键
const TOKEN_KEY = 'token'; const TOKEN_KEY = 'token';
@@ -19,13 +19,13 @@ export async function login(data: LoginRequest): Promise<BaseResult<AuthResponse
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (response.success && response.data) { if (response.success && response.data) {
clearAuthData(); // 清除旧的认证数据 clearAuthData(); // 清除旧的认证数据
console.log('登录成功,保存认证数据:', response.data); console.log('登录成功,保存认证数据:', response.data);
saveAuthData(response.data); // 保存新的认证数据 saveAuthData(response.data); // 保存新的认证数据
} }
return response; return response;
} }
@@ -96,7 +96,7 @@ export const getToken = (): string | null => {
}; };
// 处理GitHub OAuth回调,接收token并保存 // 处理GitHub OAuth回调,接收token并保存
export function handleOAuthCallback(): boolean { export async function handleOAuthCallback(): Promise<boolean> {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token'); const token = urlParams.get('token');
const error = urlParams.get('error'); const error = urlParams.get('error');
@@ -104,52 +104,40 @@ export function handleOAuthCallback(): boolean {
if (error) return false; if (error) return false;
if (token) { if (token) {
const githubUser = parseJwt(token); try {
if (githubUser) { // 保存临时token,用于API调用
// 保存token localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem('token', token);
// 获取完整的用户信息
// 保存用户信息 const userResponse = await getCurrentUser();
if (githubUser.unique_name && githubUser.email) {
const user: UserProfile = { if (userResponse.success && userResponse.data) {
id: parseInt(githubUser.nameid), // 构造完整的认证响应并保存
userName: githubUser.unique_name, const authResponse: AuthResponse = {
email: githubUser.email, token: token,
roleName: '' user: userResponse.data
}; };
localStorage.setItem('user', JSON.stringify(user));
saveAuthData(authResponse);
// 清除URL中的token参数
const url = new URL(window.location.href);
url.searchParams.delete('token');
window.history.replaceState({}, document.title, url.toString());
return true;
} }
return false;
// 清除URL中的token参数 } catch (error) {
const url = new URL(window.location.href); console.error('第三方登录处理失败:', error);
url.searchParams.delete('token'); clearAuthData(); // 清除可能部分保存的数据
window.history.replaceState({}, document.title, url.toString()); return false;
return true;
} }
} }
return false; return false;
} }
// 解析JWT获取用户信息
function parseJwt(token: string) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
}
// 获取GitHub登录URL
export function getGitHubLoginUrl(): string { export function getGitHubLoginUrl(): string {
return `${BASE_URL}/auth/github/login?returnUrl=${window.location.origin}/api/auth/github/callback`; return `${BASE_URL}/auth/github/login`;
} }
+311 -285
View File
@@ -1,305 +1,331 @@
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, GoogleOutlined} 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} from '../../api';
import useIsMobile from '../../hooks/useIsMobile'; import useIsMobile from '../../hooks/useIsMobile';
const { Title, Text } = Typography; const {Title, Text} = Typography;
const Login: React.FC = () => { const Login: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
if (handleOAuthCallback()) { const checkOAuthCallback = async () => {
message.success('GitHub登录成功!'); try {
navigate('/'); if (await handleOAuthCallback()) {
return; message.success('第三方登录成功!');
} navigate('/');
return;
if (isAuthenticated()) { }
navigate('/');
} if (isAuthenticated()) {
}, [navigate]); navigate('/');
}
const onFinish = async (values: any) => { } catch (error) {
setLoading(true); console.error('处理登录回调失败:', error);
try { message.error('登录过程中出现错误');
const response = await login({ }
email: values.email, };
password: values.password
});
if (response.success && response.data) {
// 保存认证信息
saveAuthData(response.data);
// 显示成功消息 checkOAuthCallback();
message.success(response.message || '登录成功!'); }, [navigate]);
// 跳转到首页
navigate('/');
} else {
// 显示错误消息
message.error(response.message || '登录失败,请检查账号和密码');
}
} catch (error) {
console.error('登录出错:', error);
message.error('登录过程中出现错误,请稍后重试');
} finally {
setLoading(false);
}
};
const handleGitHubLogin = () => { const onFinish = async (values: any) => {
window.location.href = getGitHubLoginUrl(); setLoading(true);
}; try {
const response = await login({
email: values.email,
password: values.password
});
return ( if (response.success && response.data) {
<Row style={{ height: '100vh', overflow: 'hidden' }}> // 保存认证信息
{/* 左侧登录表单 */} saveAuthData(response.data);
<Col
xs={24}
md={isMobile ? 24 : 12}
style={{
padding: isMobile ? '20px' : '40px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
maxWidth: isMobile ? '100%' : '650px',
margin: '0 auto'
}}
>
<div style={{
maxWidth: isMobile ? '100%' : '400px',
width: '100%',
margin: '0 auto',
paddingBottom: isMobile ? '40px' : 0
}}>
<div style={{ marginBottom: '40px', textAlign: 'center' }}>
<Title level={2} style={{
marginBottom: '8px',
fontWeight: 700,
background: 'linear-gradient(120deg, #18181b, #444444)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
}}>
Foxel
</Title>
<Text style={{ fontSize: '16px', color: '#666' }}>
使
</Text>
</div>
<Form
name="login_form" // 显示成功消息
initialValues={{ remember: true }} message.success(response.message || '登录成功!');
onFinish={onFinish}
size="large"
layout="vertical"
>
<Form.Item
name="email"
rules={[{ required: true, message: '请输入您的邮箱' }]}
>
<Input
prefix={<UserOutlined style={{ color: '#bfbfbf' }} />}
placeholder="邮箱"
style={{
height: '50px',
borderRadius: '10px',
backgroundColor: '#f8f9fa',
border: '1px solid #eaeaea'
}}
/>
</Form.Item>
<Form.Item // 跳转到首页
name="password" navigate('/');
rules={[{ required: true, message: '请输入您的密码' }]} } else {
> // 显示错误消息
<Input.Password message.error(response.message || '登录失败,请检查账号和密码');
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />} }
placeholder="密码" } catch (error) {
style={{ console.error('登录出错:', error);
height: '50px', message.error('登录过程中出现错误,请稍后重试');
borderRadius: '10px', } finally {
backgroundColor: '#f8f9fa', setLoading(false);
border: '1px solid #eaeaea' }
}} };
/>
</Form.Item>
<Form.Item> const handleGitHubLogin = () => {
<div style={{ display: 'flex', justifyContent: 'space-between' }}> window.location.href = getGitHubLoginUrl();
<Checkbox name="remember"></Checkbox> };
<a href="#forgot" style={{ color: '#18181b' }}></a>
</div>
</Form.Item>
<Form.Item> return (
<Button <Row style={{height: '100vh', overflow: 'hidden'}}>
type="primary" {/* 左侧登录表单 */}
htmlType="submit" <Col
loading={loading} xs={24}
style={{ md={isMobile ? 24 : 12}
width: '100%', style={{
height: '50px', padding: isMobile ? '20px' : '40px',
borderRadius: '10px', display: 'flex',
fontWeight: 500, flexDirection: 'column',
fontSize: '16px', justifyContent: 'center',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)' maxWidth: isMobile ? '100%' : '650px',
margin: '0 auto'
}} }}
> >
<div style={{
</Button> maxWidth: isMobile ? '100%' : '400px',
</Form.Item> width: '100%',
margin: '0 auto',
paddingBottom: isMobile ? '40px' : 0
}}>
<div style={{marginBottom: '40px', textAlign: 'center'}}>
<Title level={2} style={{
marginBottom: '8px',
fontWeight: 700,
background: 'linear-gradient(120deg, #18181b, #444444)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
}}>
Foxel
</Title>
<Text style={{fontSize: '16px', color: '#666'}}>
使
</Text>
</div>
<Divider plain style={{ color: '#999', fontSize: '14px' }}> <Form
使 name="login_form"
</Divider> initialValues={{remember: true}}
onFinish={onFinish}
size="large"
layout="vertical"
>
<Form.Item
name="email"
rules={[{required: true, message: '请输入您的邮箱'}]}
>
<Input
prefix={<UserOutlined style={{color: '#bfbfbf'}}/>}
placeholder="邮箱"
style={{
height: '50px',
borderRadius: '10px',
backgroundColor: '#f8f9fa',
border: '1px solid #eaeaea'
}}
/>
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'center', gap: '20px', margin: '20px 0' }}> <Form.Item
<Button name="password"
icon={<GithubOutlined />} rules={[{required: true, message: '请输入您的密码'}]}
size="large" >
shape="circle" <Input.Password
onClick={handleGitHubLogin} prefix={<LockOutlined style={{color: '#bfbfbf'}}/>}
style={{ placeholder="密码"
backgroundColor: '#f6f6f6', style={{
border: 'none', height: '50px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)' borderRadius: '10px',
}} backgroundColor: '#f8f9fa',
/> border: '1px solid #eaeaea'
<Button }}
icon={<GoogleOutlined />} />
size="large" </Form.Item>
shape="circle"
style={{
backgroundColor: '#f6f6f6',
border: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}
/>
</div>
<div style={{ textAlign: 'center', marginTop: '30px' }}> <Form.Item>
<Text style={{ color: '#666' }}> <div style={{display: 'flex', justifyContent: 'space-between'}}>
<Link to="/register" style={{ color: '#18181b', fontWeight: 500 }}></Link> <Checkbox name="remember"></Checkbox>
</Text> <a href="#forgot" style={{color: '#18181b'}}></a>
<div style={{ marginTop: '15px' }}> </div>
<Link to="/anonymous" style={{ color: '#666' }}> </Form.Item>
<Button type="link" style={{ padding: '0', fontWeight: 500 }}></Button>
</Link>
</div>
</div>
</Form>
</div>
</Col>
{/* 右侧视觉区域 - 仅在非移动设备上显示 */} <Form.Item>
{!isMobile && ( <Button
<Col md={12} style={{ type="primary"
background: 'linear-gradient(135deg, #18181b 0%, #444444 100%)', htmlType="submit"
display: 'flex', loading={loading}
alignItems: 'center', style={{
justifyContent: 'center', width: '100%',
position: 'relative', height: '50px',
overflow: 'hidden', borderRadius: '10px',
height: '100vh' fontWeight: 500,
}}> fontSize: '16px',
<div style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
position: 'absolute', }}
width: '600px', >
height: '600px',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%)', </Button>
top: '20%', </Form.Item>
left: '30%'
}}></div> <Divider plain style={{color: '#999', fontSize: '14px'}}>
使
<div style={{ </Divider>
position: 'absolute',
width: '400px', <div style={{display: 'flex', justifyContent: 'center', gap: '20px', margin: '20px 0'}}>
height: '400px', <Button
background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 70%)', icon={<GithubOutlined/>}
bottom: '10%', size="large"
right: '20%' shape="circle"
}}></div> onClick={handleGitHubLogin}
style={{
<div style={{ backgroundColor: '#f6f6f6',
padding: '40px', border: 'none',
textAlign: 'center', boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
color: 'white', }}
position: 'relative', />
zIndex: 1, <Button
maxWidth: '500px' icon={<GoogleOutlined/>}
}}> size="large"
<Title level={2} style={{ shape="circle"
color: 'white', style={{
marginBottom: '25px', backgroundColor: '#f6f6f6',
fontWeight: 700, border: 'none',
letterSpacing: '1px' boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}> }}
/>
</Title> </div>
<Text style={{
fontSize: '18px', <div style={{textAlign: 'center', marginTop: '30px'}}>
color: 'rgba(255,255,255,0.8)', <Text style={{color: '#666'}}>
lineHeight: '1.8', <Link to="/register"
display: 'block', style={{color: '#18181b', fontWeight: 500}}></Link>
marginBottom: '30px' </Text>
}}> <div style={{marginTop: '15px'}}>
Foxel <Link to="/anonymous" style={{color: '#666'}}>
</Text> <Button type="link" style={{padding: '0', fontWeight: 500}}></Button>
</Link>
{/* 图片管理界面预览 */} </div>
<div style={{ </div>
width: '100%', </Form>
height: '300px', </div>
background: 'rgba(255,255,255,0.1)', </Col>
borderRadius: '20px',
boxShadow: '0 20px 40px rgba(0,0,0,0.3)', {/* 右侧视觉区域 - 仅在非移动设备上显示 */}
backdropFilter: 'blur(20px)', {!isMobile && (
border: '1px solid rgba(255,255,255,0.2)', <Col md={12} style={{
position: 'relative', background: 'linear-gradient(135deg, #18181b 0%, #444444 100%)',
overflow: 'hidden' display: 'flex',
}}> alignItems: 'center',
<div style={{ justifyContent: 'center',
position: 'absolute', position: 'relative',
top: '12px', overflow: 'hidden',
left: '12px', height: '100vh'
right: '12px', }}>
height: '30px', <div style={{
borderRadius: '8px 8px 0 0', position: 'absolute',
display: 'flex', width: '600px',
alignItems: 'center', height: '600px',
gap: '8px', background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%)',
paddingLeft: '12px' top: '20%',
}}> left: '30%'
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: 'rgba(255,255,255,0.6)' }} /> }}></div>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: 'rgba(255,255,255,0.6)' }} />
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: 'rgba(255,255,255,0.6)' }} /> <div style={{
</div> position: 'absolute',
width: '400px',
<div style={{ height: '400px',
position: 'absolute', background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 70%)',
top: '50%', bottom: '10%',
left: '50%', right: '20%'
transform: 'translate(-50%, -50%)', }}></div>
fontSize: '24px',
fontWeight: 'bold', <div style={{
color: 'rgba(255,255,255,0.7)' padding: '40px',
}}> textAlign: 'center',
Foxel color: 'white',
</div> position: 'relative',
</div> zIndex: 1,
</div> maxWidth: '500px'
</Col> }}>
)} <Title level={2} style={{
</Row> color: 'white',
); marginBottom: '25px',
fontWeight: 700,
letterSpacing: '1px'
}}>
</Title>
<Text style={{
fontSize: '18px',
color: 'rgba(255,255,255,0.8)',
lineHeight: '1.8',
display: 'block',
marginBottom: '30px'
}}>
Foxel
</Text>
{/* 图片管理界面预览 */}
<div style={{
width: '100%',
height: '300px',
background: 'rgba(255,255,255,0.1)',
borderRadius: '20px',
boxShadow: '0 20px 40px rgba(0,0,0,0.3)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.2)',
position: 'relative',
overflow: 'hidden'
}}>
<div style={{
position: 'absolute',
top: '12px',
left: '12px',
right: '12px',
height: '30px',
borderRadius: '8px 8px 0 0',
display: 'flex',
alignItems: 'center',
gap: '8px',
paddingLeft: '12px'
}}>
<div style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.6)'
}}/>
<div style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.6)'
}}/>
<div style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.6)'
}}/>
</div>
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: '24px',
fontWeight: 'bold',
color: 'rgba(255,255,255,0.7)'
}}>
Foxel
</div>
</div>
</div>
</Col>
)}
</Row>
);
}; };
export default Login; export default Login;
+17 -9
View File
@@ -13,16 +13,24 @@ const Register: React.FC = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
// 处理可能的OAuth回调 const checkOAuthCallback = async () => {
if (handleOAuthCallback()) { try {
message.success('使用GitHub账号注册成功!'); if (await handleOAuthCallback()) {
navigate('/'); message.success('使用GitHub账号注册成功!');
return; navigate('/');
} return;
}
if (isAuthenticated()) { if (isAuthenticated()) {
navigate('/'); navigate('/');
} }
} catch (error) {
console.error('处理登录回调失败:', error);
message.error('登录过程中出现错误');
}
};
checkOAuthCallback();
}, [navigate]); }, [navigate]);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
+4 -2
View File
@@ -138,12 +138,14 @@ const SystemConfig: React.FC = () => {
groupName="Authentication" groupName="Authentication"
configs={{ configs={{
"GitHubClientId": configs.Authentication?.["GitHubClientId"] || '', "GitHubClientId": configs.Authentication?.["GitHubClientId"] || '',
"GitHubClientSecret": configs.Authentication?.["GitHubClientSecret"] || '' "GitHubClientSecret": configs.Authentication?.["GitHubClientSecret"] || '',
"GitHubCallbackUrl": configs.Authentication?.["GitHubCallbackUrl"] || ''
}} }}
onSave={(_group, key, value) => handleSaveConfig('Authentication', key, value)} onSave={(_group, key, value) => handleSaveConfig('Authentication', key, value)}
descriptions={{ descriptions={{
"GitHubClientId": 'GitHub OAuth 应用客户端ID', "GitHubClientId": 'GitHub OAuth 应用客户端ID',
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥' "GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}} }}
/> />
</TabPane> </TabPane>