Skip to content

Commit 85ab79d

Browse files
committed
feat: Implement Gitee login functionality and enhance user registration process
1 parent 28db53a commit 85ab79d

File tree

8 files changed

+295
-13
lines changed

8 files changed

+295
-13
lines changed

src/KoalaWiki/Functions/FileFunction.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ public async Task<string> ReadItem(
209209
limit = int.MaxValue;
210210
}
211211

212+
if (DocumentContext.DocumentStore?.Files != null)
213+
{
214+
DocumentContext.DocumentStore.Files.Add(filePath);
215+
}
216+
212217
// 先读取整个文件内容
213218
string fileContent = await File.ReadAllTextAsync(filePath);
214219

src/KoalaWiki/Infrastructure/DocumentsHelper.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ public static void ProcessCatalogueItems(List<DocumentResultCatalogueItem> items
2727
foreach (var item in items)
2828
{
2929
item.title = item.title.Replace(" ", "");
30+
31+
var url = string.IsNullOrEmpty(parentTitle) ? item.title : $"{parentTitle}_{item.title}";
3032
var documentItem = new DocumentCatalog
3133
{
3234
WarehouseId = warehouse.Id,
3335
Description = item.title,
3436
Id = Guid.NewGuid() + item.title,
3537
Name = item.name,
36-
Url = parentTitle + "_" + item.title,
38+
Url = url,
3739
DucumentId = document.Id,
3840
ParentId = parentId,
3941
Prompt = item.prompt,

src/KoalaWiki/KoalaWarehouse/DocumentPending/DocumentPendingService.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ await dbContext.DocumentCatalogs.Where(x => x.Id == catalog.Id)
172172
int retryCount = 0;
173173
const int retries = 5;
174174
var files = new List<string>();
175-
DocumentContext.DocumentStore = new DocumentStore();
176175

177176
while (true)
178177
{
@@ -184,10 +183,8 @@ await dbContext.DocumentCatalogs.Where(x => x.Id == catalog.Id)
184183
}
185184

186185
Log.Logger.Information("处理仓库;{path} ,处理标题:{name}", path, catalog.Name);
187-
var fileItem = await ProcessCatalogueItems(catalog, kernel, catalogue, gitRepository, branch, path,
188-
classifyType);
189-
files.AddRange(DocumentContext.DocumentStore.Files);
190-
186+
var fileItem = await ProcessCatalogueItems(catalog, catalogue, gitRepository, branch, path,
187+
classifyType, files);
191188
// ProcessCatalogueItems内部已经进行了质量验证,这里只做最终检查
192189
if (fileItem == null)
193190
{
@@ -239,10 +236,11 @@ await dbContext.DocumentCatalogs.Where(x => x.Id == catalog.Id)
239236
/// <summary>
240237
/// 处理每一个标题产生文件内容
241238
/// </summary>
242-
private static async Task<DocumentFileItem> ProcessCatalogueItems(DocumentCatalog catalog, Kernel kernel,
239+
private static async Task<DocumentFileItem> ProcessCatalogueItems(DocumentCatalog catalog,
243240
string codeFiles,
244-
string gitRepository, string branch, string path, ClassifyType? classify)
241+
string gitRepository, string branch, string path, ClassifyType? classify, List<string> files)
245242
{
243+
DocumentContext.DocumentStore = new DocumentStore();
246244
// 为每个文档处理创建独立的Kernel实例,避免状态管理冲突
247245
var documentKernel = KernelFactory.GetKernel(
248246
OpenAIOptions.Endpoint,
@@ -441,6 +439,8 @@ 7. ENSURE all enhancements are based on the code files analyzed in the original
441439
Title = catalog.Name,
442440
};
443441

442+
files.AddRange(DocumentContext.DocumentStore.Files);
443+
444444
return fileItem;
445445
}
446446

@@ -470,6 +470,7 @@ public static (bool IsValid, string ValidationMessage, DocumentQualityMetrics Me
470470
validationIssues.Add("文档内容为空");
471471
return (false, string.Join("; ", validationIssues), metrics);
472472
}
473+
473474
// 设置整体质量评分
474475
metrics.QualityScore = CalculateQualityScore(metrics, validationIssues.Count);
475476

src/KoalaWiki/Services/AuthService.cs

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.IdentityModel.Tokens.Jwt;
2+
using System.Net.Http.Headers;
23
using System.Security.Claims;
34
using System.Text;
45
using System.Text.Json;
@@ -7,8 +8,8 @@
78
using KoalaWiki.Domains.Users;
89
using KoalaWiki.Dto;
910
using MapsterMapper;
10-
using Microsoft.EntityFrameworkCore;
1111
using Octokit;
12+
using ProductHeaderValue = Octokit.ProductHeaderValue;
1213
using User = KoalaWiki.Domains.Users.User;
1314

1415
namespace KoalaWiki.Services;
@@ -188,6 +189,168 @@ public async Task<LoginDto> RegisterAsync(RegisterInput input)
188189
}
189190
}
190191

192+
/// <summary>
193+
/// Gitee登录
194+
/// </summary>
195+
/// <param name="code">授权码</param>
196+
/// <returns>登录结果</returns>
197+
public async Task<LoginDto> GiteeLoginAsync(string code)
198+
{
199+
try
200+
{
201+
var clientId = configuration["Gitee:ClientId"];
202+
var clientSecret = configuration["Gitee:ClientSecret"];
203+
var redirectUri = configuration["Gitee:RedirectUri"];
204+
205+
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
206+
{
207+
return new LoginDto(false, string.Empty, null, null, "Gitee配置错误");
208+
}
209+
210+
// 获取访问令牌
211+
using var httpClient = new HttpClient();
212+
var tokenRequest = new FormUrlEncodedContent(new[]
213+
{
214+
new KeyValuePair<string, string>("grant_type", "authorization_code"),
215+
new KeyValuePair<string, string>("code", code),
216+
new KeyValuePair<string, string>("client_id", clientId),
217+
new KeyValuePair<string, string>("client_secret", clientSecret),
218+
new KeyValuePair<string, string>("redirect_uri", redirectUri ?? "")
219+
});
220+
221+
var tokenResponse = await httpClient.PostAsync("https://gitee.com/oauth/token", tokenRequest);
222+
if (!tokenResponse.IsSuccessStatusCode)
223+
{
224+
return new LoginDto(false, string.Empty, null, null, "Gitee授权失败");
225+
}
226+
227+
var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
228+
var tokenData = JsonSerializer.Deserialize<JsonElement>(tokenContent);
229+
230+
if (!tokenData.TryGetProperty("access_token", out var accessTokenElement))
231+
{
232+
return new LoginDto(false, string.Empty, null, null, "获取Gitee访问令牌失败");
233+
}
234+
235+
var accessToken = accessTokenElement.GetString();
236+
if (string.IsNullOrEmpty(accessToken))
237+
{
238+
return new LoginDto(false, string.Empty, null, null, "Gitee访问令牌为空");
239+
}
240+
241+
// 获取用户信息
242+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
243+
var userResponse = await httpClient.GetAsync("https://gitee.com/api/v5/user");
244+
245+
if (!userResponse.IsSuccessStatusCode)
246+
{
247+
return new LoginDto(false, string.Empty, null, null, "获取Gitee用户信息失败");
248+
}
249+
250+
var userContent = await userResponse.Content.ReadAsStringAsync();
251+
var userData = JsonSerializer.Deserialize<JsonElement>(userContent);
252+
253+
var giteeUserId = userData.GetProperty("id").GetInt64().ToString();
254+
var giteeLogin = userData.GetProperty("login").GetString() ?? "";
255+
var giteeEmail = userData.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : "";
256+
var giteeAvatar = userData.TryGetProperty("avatar_url", out var avatarElement) ? avatarElement.GetString() : "";
257+
258+
// 查询用户是否存在
259+
var userInAuth = await dbContext.UserInAuths.FirstOrDefaultAsync(u =>
260+
u.Id == giteeUserId && u.Provider == "Gitee");
261+
262+
User user = null;
263+
if (userInAuth != null)
264+
{
265+
user = await dbContext.Users
266+
.FirstOrDefaultAsync(u => u.Id == userInAuth.UserId);
267+
}
268+
269+
// 用户不存在,自动注册
270+
if (user == null)
271+
{
272+
user = new User
273+
{
274+
Id = Guid.NewGuid().ToString("N"),
275+
Name = giteeLogin,
276+
Email = giteeEmail ?? string.Empty,
277+
Password = Guid.NewGuid().ToString(), // 随机密码
278+
Avatar = giteeAvatar,
279+
CreatedAt = DateTime.UtcNow,
280+
};
281+
282+
// 绑定Gitee账号
283+
userInAuth = new UserInAuth
284+
{
285+
Id = giteeUserId,
286+
UserId = user.Id,
287+
Provider = "Gitee",
288+
CreatedAt = DateTime.UtcNow
289+
};
290+
291+
// 获取普通用户角色
292+
var userRole = await dbContext.Roles
293+
.FirstOrDefaultAsync(r => r.Name == "user");
294+
295+
await dbContext.UserInRoles.AddAsync(new UserInRole
296+
{
297+
UserId = user.Id,
298+
RoleId = userRole!.Id
299+
});
300+
301+
// 保存用户
302+
await dbContext.Users.AddAsync(user);
303+
await dbContext.UserInAuths.AddAsync(userInAuth);
304+
await dbContext.SaveChangesAsync();
305+
}
306+
307+
// 更新登录信息
308+
user.LastLoginAt = DateTime.UtcNow;
309+
user.LastLoginIp = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
310+
311+
if (httpContextAccessor.HttpContext?.Request.Headers["x-forwarded-for"].Count > 0)
312+
{
313+
user.LastLoginIp = httpContextAccessor.HttpContext.Request.Headers["x-forwarded-for"];
314+
}
315+
else if (httpContextAccessor.HttpContext?.Request.Headers["x-real-ip"].Count > 0)
316+
{
317+
user.LastLoginIp = httpContextAccessor.HttpContext.Request.Headers["x-real-ip"];
318+
}
319+
320+
await dbContext.SaveChangesAsync();
321+
322+
// 获取当前用户的角色
323+
var roleIds = await dbContext.UserInRoles
324+
.Where(ur => ur.UserId == user.Id)
325+
.Select(x => x.RoleId)
326+
.ToListAsync();
327+
328+
var roles = await dbContext.Roles
329+
.Where(r => roleIds.Contains(r.Id))
330+
.ToListAsync();
331+
332+
// 生成JWT令牌
333+
var jwtToken = GenerateJwtToken(user, roles);
334+
var refreshToken = GenerateRefreshToken(user);
335+
336+
var userDto = mapper.Map<UserInfoDto>(user);
337+
userDto.Role = string.Join(',', roles.Select(x => x.Name));
338+
339+
// 设置到cookie
340+
httpContextAccessor.HttpContext?.Response.Cookies.Append("refreshToken", refreshToken,
341+
CreateCookieOptions(jwtOptions.RefreshExpireMinutes));
342+
httpContextAccessor.HttpContext?.Response.Cookies.Append("token", jwtToken,
343+
CreateCookieOptions(jwtOptions.ExpireMinutes));
344+
345+
return new LoginDto(true, jwtToken, refreshToken, userDto, null);
346+
}
347+
catch (Exception ex)
348+
{
349+
logger.LogError(ex, "Gitee登录失败");
350+
return new LoginDto(false, string.Empty, null, null, "Gitee登录失败,请稍后再试");
351+
}
352+
}
353+
191354
/// <summary>
192355
/// GitHub登录
193356
/// </summary>
@@ -619,6 +782,19 @@ public async Task<List<SupportedThirdPartyLoginsDto>> GetSupportedThirdPartyLogi
619782
});
620783
}
621784

785+
// 检查Gitee配置
786+
if (!string.IsNullOrEmpty(configuration["Gitee:ClientId"]) &&
787+
!string.IsNullOrEmpty(configuration["Gitee:ClientSecret"]))
788+
{
789+
supportedLogins.Add(new SupportedThirdPartyLoginsDto
790+
{
791+
Name = "Gitee",
792+
Icon = "https://gitee.com/favicon.ico",
793+
ClientId = configuration["Gitee:ClientId"] ?? string.Empty,
794+
RedirectUri = configuration["Gitee:RedirectUri"] ?? string.Empty
795+
});
796+
}
797+
622798
return await Task.FromResult(supportedLogins);
623799
}
624800

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
5+
interface FileSource {
6+
id: string;
7+
name: string;
8+
}
9+
10+
interface FileDependenciesProps {
11+
files: FileSource[];
12+
}
13+
14+
export default function FileDependencies({ files }: FileDependenciesProps) {
15+
const [isExpanded, setIsExpanded] = useState(false);
16+
17+
return (
18+
<div className="mb-6 border border-gray-200 rounded-lg bg-gray-50">
19+
<button
20+
onClick={() => setIsExpanded(!isExpanded)}
21+
className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-gray-100 transition-colors"
22+
>
23+
<div className="flex items-center gap-2">
24+
<span className="text-sm font-medium text-gray-700">
25+
文档依赖文件
26+
</span>
27+
<span className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
28+
{files.length}
29+
</span>
30+
</div>
31+
<svg
32+
className={`w-4 h-4 text-gray-500 transition-transform ${
33+
isExpanded ? 'rotate-180' : ''
34+
}`}
35+
fill="none"
36+
stroke="currentColor"
37+
viewBox="0 0 24 24"
38+
>
39+
<path
40+
strokeLinecap="round"
41+
strokeLinejoin="round"
42+
strokeWidth={2}
43+
d="M19 9l-7 7-7-7"
44+
/>
45+
</svg>
46+
</button>
47+
48+
{isExpanded && (
49+
<div className="border-t border-gray-200">
50+
<div className="max-h-48 overflow-y-auto p-4">
51+
<div className="space-y-2">
52+
{files.map((file) => (
53+
<div
54+
key={file.id}
55+
className="text-sm text-gray-600 py-1 px-2 bg-white rounded border border-gray-100 hover:border-gray-200 transition-colors"
56+
>
57+
{file.name}
58+
</div>
59+
))}
60+
</div>
61+
</div>
62+
</div>
63+
)}
64+
</div>
65+
);
66+
}

web/app/[owner]/[name]/[path]/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { documentById, getWarehouseOverview } from '../../../services/warehouseS
1212
import { DocumentData, Heading } from './types';
1313
import FloatingChatClient from '../FloatingChatClient';
1414
import HeadingAnchors from '@/app/components/HeadingAnchors';
15+
import FileDependencies from './FileDependencies';
1516

1617
// 生成SEO友好的描述
1718
function generateSEODescription(document: DocumentData, owner: string, name: string, path: string): string {
@@ -246,6 +247,8 @@ export default async function DocumentPage({
246247

247248
try {
248249
const response = await documentById(owner, name, path, branch, lang);
250+
console.log('response', response.data);
251+
249252
if (response.isSuccess && response.data) {
250253
document = response.data as DocumentData;
251254
} else {
@@ -296,6 +299,9 @@ export default async function DocumentPage({
296299
{document?.description}
297300
</DocsDescription>
298301
<DocsBody>
302+
{document.fileSource.length > 0 && (
303+
<FileDependencies files={document.fileSource} />
304+
)}
299305
{compiled.body}
300306
<HeadingAnchors />
301307
</DocsBody>

0 commit comments

Comments
 (0)