Skip to content

Commit 5ed798e

Browse files
committed
Start implementing shared resource ownership, refactor git service
1 parent e284613 commit 5ed798e

File tree

29 files changed

+479
-264
lines changed

29 files changed

+479
-264
lines changed

backend/NXTBackend.API.Core/Services/Implementation/GitService.cs

Lines changed: 174 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -10,114 +10,88 @@
1010
using System.Text;
1111
using System.Text.Json;
1212
using Microsoft.AspNetCore.Http;
13+
using Microsoft.EntityFrameworkCore.Metadata.Internal;
1314
using Microsoft.Extensions.Caching.Distributed;
15+
using Microsoft.Extensions.Configuration;
1416
using Microsoft.Extensions.Logging;
1517
using NXTBackend.API.Core.Services.Implementation;
1618
using NXTBackend.API.Core.Services.Interface;
1719
using NXTBackend.API.Core.Utils;
1820
using NXTBackend.API.Domain.Entities;
21+
using NXTBackend.API.Domain.Entities.Users;
1922
using NXTBackend.API.Domain.Enums;
2023
using NXTBackend.API.Infrastructure.Database;
2124
using NXTBackend.API.Models;
25+
using NXTBackend.API.Models.Requests.Git;
26+
using NXTBackend.API.Models.Responses.Git;
2227
using NXTBackend.API.Models.Responses.Gitea;
2328

2429
namespace NXTBackend.API.Core.Services;
2530

26-
public sealed class GitService : BaseService<Git>, IGitService
31+
public sealed class GitService(
32+
DatabaseContext ctx,
33+
IConfiguration config,
34+
ILogger<GitService> logger,
35+
IHttpClientFactory clientFactory,
36+
IHttpContextAccessor httpContext) : BaseService<Git>(ctx), IGitService
2737
{
28-
private readonly HttpClient _httpClient;
29-
private readonly IDistributedCache _cache;
30-
private readonly ILogger<GitService> _logger;
31-
private readonly IHttpContextAccessor _httpContextAccessor;
32-
33-
public GitService(
34-
DatabaseContext ctx,
35-
IDistributedCache cache,
36-
IHttpClientFactory clientFactory,
37-
ILogger<GitService> logger,
38-
IHttpContextAccessor httpContextAccessor) : base(ctx)
39-
{
40-
_httpClient = clientFactory.CreateClient("NXTGit");
41-
_httpContextAccessor = httpContextAccessor;
42-
_cache = cache;
43-
_logger = logger;
44-
}
38+
private readonly HttpClient _client = clientFactory.CreateClient("NXTGit");
39+
40+
private readonly string _gitTemplate = config.GetValue<string>("GitTemplate")
41+
?? throw new InvalidDataException("A git template is required!");
4542

46-
private async Task SetAuthorizationHeaderAsync()
43+
private void SetAuthorizationHeaderAsync()
4744
{
48-
// TODO: this is *fine* but we need to make sure our proxy is set up correctly
49-
var httpContext = _httpContextAccessor.HttpContext;
50-
string? claim = httpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
45+
var ctx = httpContext.HttpContext;
46+
string? claim = ctx?.User.FindFirstValue(ClaimTypes.NameIdentifier);
5147
var id = Guid.TryParse(claim, out var guid) ? guid : Guid.Empty;
5248
if (id == Guid.Empty)
5349
throw new ServiceException(StatusCodes.Status401Unauthorized, "Unauthorized");
5450

55-
string cacheKey = $"USER_{guid}";
56-
string? cachedUser = await _cache.GetStringAsync(cacheKey);
57-
if (string.IsNullOrEmpty(cachedUser))
58-
throw new ServiceException(StatusCodes.Status422UnprocessableEntity, "No login found");
51+
logger.LogInformation("Request as user: {user}", claim);
52+
_client.DefaultRequestHeaders.Add("X-WEBAUTH-USER", claim);
53+
}
54+
55+
public Task<bool> AddCollaborator(Guid entityId, Guid userId)
56+
{
57+
SetAuthorizationHeaderAsync();
5958

60-
_logger.LogInformation("Request as user: {user}", cachedUser);
61-
_httpClient.DefaultRequestHeaders.Add("X-WEBAUTH-USER", cachedUser);
59+
throw new NotImplementedException();
6260
}
6361

64-
public async Task<Git> CreateRemoteRepository(string repositoryName, string? description = null)
62+
public async Task<Git> CreateRepository(GitRepoPostRequestDTO DTO, OwnerKind OwnerType)
6563
{
66-
await SetAuthorizationHeaderAsync();
67-
var createOption = new CreateRepoOption
68-
{
69-
Name = repositoryName,
70-
Description = description,
71-
Private = false,
72-
AutoInit = true
73-
};
64+
SetAuthorizationHeaderAsync();
7465

75-
var response = await _httpClient.PostAsJsonAsync("/api/v1/user/repos", createOption);
76-
_logger.LogInformation("Response: {response}", response);
77-
response.EnsureSuccessStatusCode();
66+
var response = await _client.PostAsJsonAsync($"/api/v1/repos/{_gitTemplate}/generate", DTO);
67+
logger.LogInformation("Response: {response}", response);
7868

79-
var repoResponse = await response.Content.ReadFromJsonAsync<JsonElement>();
69+
if (response.StatusCode is HttpStatusCode.Conflict)
70+
throw new ServiceException(StatusCodes.Status409Conflict, "The repository with the same name already exists");
8071

81-
// Map the response to our Git entity
72+
response.EnsureSuccessStatusCode();
73+
var model = await response.Content.ReadFromJsonAsync<RepoDO>() ??
74+
throw new ServiceException(StatusCodes.Status500InternalServerError, "Model mistmatch");
8275
var git = new Git
8376
{
84-
Name = repoResponse.GetProperty("name").GetString() ?? string.Empty,
85-
Namespace = repoResponse.GetProperty("full_name").GetString() ?? string.Empty,
86-
Url = repoResponse.GetProperty("clone_url").GetString() ?? string.Empty,
87-
Branch = repoResponse.GetProperty("default_branch").GetString() ?? "main",
88-
ProviderType = GitProviderKind.Managed,
89-
OwnerType = GitOwnerKind.User // TODO: Once we support organizations within the app, this needs to be configurable
77+
Name = model.Name,
78+
Namespace = model.FullName,
79+
Url = model.CloneUrl,
80+
Branch = model.DefaultBranch,
81+
ProviderType = GitProviderKind.Managed, // This as well needs to be configurable
82+
OwnerType = OwnerType
9083
};
9184

9285
return await CreateAsync(git);
9386
}
9487

95-
public async Task ArchiveRepository(string owner, string repo)
96-
{
97-
await SetAuthorizationHeaderAsync();
98-
var editOption = new EditRepoOption
99-
{
100-
Archived = true
101-
};
102-
103-
var response = await _httpClient.PatchAsync($"/api/v1/repos/{owner}/{repo}", JsonContent.Create(editOption));
104-
response.EnsureSuccessStatusCode();
105-
}
10688

107-
public async Task<FileContentResponse> UpsertFile(
108-
string owner,
109-
string repo,
110-
string path,
111-
string content,
112-
string message,
113-
string branch = "main")
89+
public async Task<string> GetFile(string GitNamespace, string Path, string Branch = "main")
11490
{
115-
await SetAuthorizationHeaderAsync();
91+
SetAuthorizationHeaderAsync();
11692

117-
// First try to get the file to check if it exists
118-
var getFileResponse = await _httpClient.GetAsync($"/api/v1/repos/{owner}/{repo}/contents/{path}");
11993
string? sha = null;
120-
94+
var getFileResponse = await _client.GetAsync($"/api/v1/repos/{GitNamespace}/contents/{Path}");
12195
if (getFileResponse.IsSuccessStatusCode)
12296
{
12397
var existingFile = await getFileResponse.Content.ReadFromJsonAsync<ContentInfo>();
@@ -149,20 +123,139 @@ public async Task<FileContentResponse> UpsertFile(
149123
response.EnsureSuccessStatusCode();
150124
return await response.Content.ReadFromJsonAsync<FileContentResponse>()
151125
?? throw new InvalidOperationException("Failed to upsert file");
126+
152127
}
153128

154-
public async Task<string> GetRawFileContent(
155-
string name,
156-
string path,
157-
string branch = "main")
129+
public Task<(Git?, User?)> IsCollaborator(Guid entityId, Guid userId)
158130
{
159-
await SetAuthorizationHeaderAsync();
131+
SetAuthorizationHeaderAsync();
160132

161-
var requestUri = $"/api/v1/repos/{name}/raw/{path}?ref={branch}";
162-
var response = await _httpClient.GetAsync(requestUri);
163-
if (response.StatusCode == HttpStatusCode.NotFound)
164-
throw new ServiceException(StatusCodes.Status404NotFound, "File not found");
165-
response.EnsureSuccessStatusCode();
166-
return await response.Content.ReadAsStringAsync();
133+
throw new NotImplementedException();
134+
}
135+
136+
public Task<bool> RemoveCollaborator(Guid entityId, Guid userId)
137+
{
138+
SetAuthorizationHeaderAsync();
139+
140+
throw new NotImplementedException();
167141
}
142+
143+
public Task SetFile(string GitNamespace, string Path, string Content, string CommitMessage, string Branch = "main")
144+
{
145+
SetAuthorizationHeaderAsync();
146+
147+
throw new NotImplementedException();
148+
}
149+
150+
public Task<Git> UpdateRepository(string GitNamespace, GitRepoPatchRequestDTO DTO)
151+
{
152+
SetAuthorizationHeaderAsync();
153+
154+
throw new NotImplementedException();
155+
}
156+
157+
// public async Task<Git> CreateRemoteRepository(string repositoryName, string? description = null)
158+
// {
159+
// await SetAuthorizationHeaderAsync();
160+
// var createOption = new CreateRepoOption
161+
// {
162+
// Name = repositoryName,
163+
// Description = description,
164+
// Private = false,
165+
// AutoInit = true
166+
// };
167+
168+
// var response = await _httpClient.PostAsJsonAsync("/api/v1/user/repos", createOption);
169+
// _logger.LogInformation("Response: {response}", response);
170+
// response.EnsureSuccessStatusCode();
171+
172+
// var repoResponse = await response.Content.ReadFromJsonAsync<JsonElement>();
173+
174+
// // Map the response to our Git entity
175+
// var git = new Git
176+
// {
177+
// Name = repoResponse.GetProperty("name").GetString() ?? string.Empty,
178+
// Namespace = repoResponse.GetProperty("full_name").GetString() ?? string.Empty,
179+
// Url = repoResponse.GetProperty("clone_url").GetString() ?? string.Empty,
180+
// Branch = repoResponse.GetProperty("default_branch").GetString() ?? "main",
181+
// ProviderType = GitProviderKind.Managed,
182+
// OwnerType = GitOwnerKind.User // TODO: Once we support organizations within the app, this needs to be configurable
183+
// };
184+
185+
// return await CreateAsync(git);
186+
// }
187+
188+
// public async Task ArchiveRepository(string owner, string repo)
189+
// {
190+
// await SetAuthorizationHeaderAsync();
191+
// var editOption = new EditRepoOption
192+
// {
193+
// Archived = true
194+
// };
195+
196+
// var response = await _httpClient.PatchAsync($"/api/v1/repos/{owner}/{repo}", JsonContent.Create(editOption));
197+
// response.EnsureSuccessStatusCode();
198+
// }
199+
200+
// public async Task<FileContentResponse> UpsertFile(
201+
// string owner,
202+
// string repo,
203+
// string path,
204+
// string content,
205+
// string message,
206+
// string branch = "main")
207+
// {
208+
// await SetAuthorizationHeaderAsync();
209+
210+
// // First try to get the file to check if it exists
211+
// var getFileResponse = await _httpClient.GetAsync($"/api/v1/repos/{owner}/{repo}/contents/{path}");
212+
// string? sha = null;
213+
214+
// if (getFileResponse.IsSuccessStatusCode)
215+
// {
216+
// var existingFile = await getFileResponse.Content.ReadFromJsonAsync<ContentInfo>();
217+
// sha = existingFile?.Sha;
218+
// }
219+
220+
// var fileContent = new FileContentRequest
221+
// {
222+
// Content = Convert.ToBase64String(Encoding.UTF8.GetBytes(content)),
223+
// Message = message,
224+
// Branch = branch,
225+
// Sha = sha,
226+
// Author = new GitUserInfo
227+
// {
228+
// Name = "NXT System",
229+
// Email = "[email protected]"
230+
// }
231+
// };
232+
233+
// var response = await _httpClient.PutAsync(
234+
// $"/api/v1/repos/{owner}/{repo}/contents/{path}",
235+
// new StringContent(
236+
// JsonSerializer.Serialize(fileContent),
237+
// Encoding.UTF8,
238+
// "application/json"
239+
// )
240+
// );
241+
242+
// response.EnsureSuccessStatusCode();
243+
// return await response.Content.ReadFromJsonAsync<FileContentResponse>()
244+
// ?? throw new InvalidOperationException("Failed to upsert file");
245+
// }
246+
247+
// public async Task<string> GetRawFileContent(
248+
// string name,
249+
// string path,
250+
// string branch = "main")
251+
// {
252+
// await SetAuthorizationHeaderAsync();
253+
254+
// var requestUri = $"/api/v1/repos/{name}/raw/{path}?ref={branch}";
255+
// var response = await _httpClient.GetAsync(requestUri);
256+
// if (response.StatusCode == HttpStatusCode.NotFound)
257+
// throw new ServiceException(StatusCodes.Status404NotFound, "File not found");
258+
// response.EnsureSuccessStatusCode();
259+
// return await response.Content.ReadAsStringAsync();
260+
// }
168261
}

backend/NXTBackend.API.Core/Services/Implementation/ProjectService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public async Task<string> GetFileFromProject(Guid projectId, string file, string
4949
var markdown = await _cache.GetStringAsync(cacheKey);
5050
if (markdown is null)
5151
{
52-
markdown = await _git.GetRawFileContent(project.GitInfo.Namespace, file, branch);
52+
markdown = await _git.GetFile(project.GitInfo.Namespace, file, branch);
5353
await _cache.SetStringAsync(cacheKey, markdown, options: new ()
5454
{
5555
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using NXTBackend.API.Core.Services.Interface;
3+
using NXTBackend.API.Domain.Common;
4+
using NXTBackend.API.Domain.Entities;
5+
using NXTBackend.API.Domain.Entities.Evaluation;
6+
using NXTBackend.API.Domain.Enums;
7+
using NXTBackend.API.Infrastructure.Database;
8+
9+
namespace NXTBackend.API.Core.Services.Implementation;
10+
11+
/// <summary>
12+
/// Temporary service to search for users, projects, cursi and learning goals.
13+
/// Later on this service SHOULD be converted to use a search engine like ElasticSearch.
14+
/// </summary>
15+
public sealed class ResourceOwnerService(IUserService userService) : IResourceOwnerService
16+
{
17+
public async Task<ResourceOwner?> FindByIdAsync(Guid id)
18+
{
19+
var user = await userService.FindByIdAsync(id);
20+
if (user is not null)
21+
return new(user, null, OwnerKind.User);
22+
return null; // TODO: Implement organizations overall...
23+
}
24+
}

0 commit comments

Comments
 (0)