From 111eca1b3c08a72795fc94feb45509d300a366cc Mon Sep 17 00:00:00 2001 From: Cardidi Date: Mon, 24 Mar 2025 01:56:37 +0800 Subject: [PATCH] feat: Add basic supports on Repository support. --- .../Request/LocateUserRequest.cs | 3 - .../Request/QueryPagesRequest.cs | 7 + .../Response/PagedResponse.cs | 2 +- .../Response/RepositoryInfoResponse.cs | 18 ++ Flawless.Communication/Shared/RepoUserRole.cs | 8 + .../Shared/RepositoryRole.cs | 9 + .../Interface/IReadonlyRepository.cs | 18 -- Flawless.Core/Interface/IRepositoryCommit.cs | 22 -- Flawless.Core/Modal/Author.cs | 2 +- Flawless.Core/Modal/CommitManifest.cs | 2 +- Flawless.Core/Modal/DepotLabel.cs | 2 +- .../Controllers/AdminUserController.cs | 21 +- .../Controllers/RepositoryControl.cs | 10 + .../Controllers/RepositoryController.cs | 9 - .../Controllers/RepositoryManageController.cs | 232 ++++++++++++++++++ Flawless.Server/Controllers/UserController.cs | 42 ++-- .../WebSocketTransferController.cs | 49 +++- Flawless.Server/Models/AppUser.cs | 6 +- Flawless.Server/Models/AppUserRefreshKey.cs | 7 +- .../Models/RepoDepotTransmissionTask.cs | 12 + Flawless.Server/Models/Repository.cs | 25 ++ Flawless.Server/Models/RepositoryCommit.cs | 20 ++ Flawless.Server/Models/RepositoryDepot.cs | 13 + Flawless.Server/Models/RepositoryMember.cs | 17 ++ Flawless.Server/Program.cs | 4 +- Flawless.Server/Services/AppDbContext.cs | 1 + .../Services/RepoTransmissionContext.cs | 15 ++ Flawless.Server/Services/RepositoryContext.cs | 17 -- .../Services/RepositoryContextFactory.cs | 12 - Flawless.Server/Utility/PathTransformer.cs | 34 +++ 30 files changed, 503 insertions(+), 136 deletions(-) delete mode 100644 Flawless.Communication/Request/LocateUserRequest.cs create mode 100644 Flawless.Communication/Response/RepositoryInfoResponse.cs create mode 100644 Flawless.Communication/Shared/RepoUserRole.cs create mode 100644 Flawless.Communication/Shared/RepositoryRole.cs delete mode 100644 Flawless.Core/Interface/IReadonlyRepository.cs delete mode 100644 Flawless.Core/Interface/IRepositoryCommit.cs create mode 100644 Flawless.Server/Controllers/RepositoryControl.cs delete mode 100644 Flawless.Server/Controllers/RepositoryController.cs create mode 100644 Flawless.Server/Controllers/RepositoryManageController.cs create mode 100644 Flawless.Server/Models/RepoDepotTransmissionTask.cs create mode 100644 Flawless.Server/Models/Repository.cs create mode 100644 Flawless.Server/Models/RepositoryCommit.cs create mode 100644 Flawless.Server/Models/RepositoryDepot.cs create mode 100644 Flawless.Server/Models/RepositoryMember.cs create mode 100644 Flawless.Server/Services/RepoTransmissionContext.cs delete mode 100644 Flawless.Server/Services/RepositoryContext.cs delete mode 100644 Flawless.Server/Services/RepositoryContextFactory.cs create mode 100644 Flawless.Server/Utility/PathTransformer.cs diff --git a/Flawless.Communication/Request/LocateUserRequest.cs b/Flawless.Communication/Request/LocateUserRequest.cs deleted file mode 100644 index 428544e..0000000 --- a/Flawless.Communication/Request/LocateUserRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Flawless.Communication.Request; - -public record LocateUserRequest(string? UserId, string? Username); \ No newline at end of file diff --git a/Flawless.Communication/Request/QueryPagesRequest.cs b/Flawless.Communication/Request/QueryPagesRequest.cs index cdb29e0..f2d9249 100644 --- a/Flawless.Communication/Request/QueryPagesRequest.cs +++ b/Flawless.Communication/Request/QueryPagesRequest.cs @@ -7,4 +7,11 @@ public class QueryPagesRequest public required int Length { get; init; } public T? Parameter { get; init; } +} + +public class QueryPagesRequest +{ + public required int Offset { get; init; } + + public required int Length { get; init; } } \ No newline at end of file diff --git a/Flawless.Communication/Response/PagedResponse.cs b/Flawless.Communication/Response/PagedResponse.cs index bfa9d01..5be4a86 100644 --- a/Flawless.Communication/Response/PagedResponse.cs +++ b/Flawless.Communication/Response/PagedResponse.cs @@ -6,7 +6,7 @@ public record PagedResponse public required int Length { get; init; } - public uint? Total { get; init; } + public int? Total { get; init; } public IEnumerable? Data { get; init; } } diff --git a/Flawless.Communication/Response/RepositoryInfoResponse.cs b/Flawless.Communication/Response/RepositoryInfoResponse.cs new file mode 100644 index 0000000..d5f792f --- /dev/null +++ b/Flawless.Communication/Response/RepositoryInfoResponse.cs @@ -0,0 +1,18 @@ +using Flawless.Communication.Shared; + +namespace Flawless.Communication.Response; + +public record RepositoryInfoResponse +{ + public required string RepositoryName { get; set; } + + public required string OwnerUsername { get; set; } + + public required Guid LatestCommitId { get; set; } + + public string? Description { get; set; } + + public required bool IsArchived { get; set; } + + public required RepositoryRole Role { get; set; } +} \ No newline at end of file diff --git a/Flawless.Communication/Shared/RepoUserRole.cs b/Flawless.Communication/Shared/RepoUserRole.cs new file mode 100644 index 0000000..daff93d --- /dev/null +++ b/Flawless.Communication/Shared/RepoUserRole.cs @@ -0,0 +1,8 @@ +namespace Flawless.Communication.Shared; + +public record RepoUserRole +{ + public required string Username { get; set; } + + public RepositoryRole? Role { get; set; } +} \ No newline at end of file diff --git a/Flawless.Communication/Shared/RepositoryRole.cs b/Flawless.Communication/Shared/RepositoryRole.cs new file mode 100644 index 0000000..1fcd596 --- /dev/null +++ b/Flawless.Communication/Shared/RepositoryRole.cs @@ -0,0 +1,9 @@ +namespace Flawless.Communication.Shared; + +public enum RepositoryRole +{ + Guest = 0, + Reporter = 1, + Developer = 2, + Owner = 3, +} \ No newline at end of file diff --git a/Flawless.Core/Interface/IReadonlyRepository.cs b/Flawless.Core/Interface/IReadonlyRepository.cs deleted file mode 100644 index a53b2c1..0000000 --- a/Flawless.Core/Interface/IReadonlyRepository.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Flawless.Core.Interface; - -/// -/// Standardized interface to describe a place to store depots and how they connected with each other. -/// -public interface IReadonlyRepository -{ - public bool IsReadonly { get; } - - public IEnumerable GetCommits(); - - public IRepositoryCommit? GetCommitById(uint commitId); - - public IAsyncEnumerable GetCommitsAsync(CancellationToken cancellationToken = default); - - public Task GetCommitByIdAsync(uint commitId, CancellationToken cancellationToken = default); - -} \ No newline at end of file diff --git a/Flawless.Core/Interface/IRepositoryCommit.cs b/Flawless.Core/Interface/IRepositoryCommit.cs deleted file mode 100644 index 606d461..0000000 --- a/Flawless.Core/Interface/IRepositoryCommit.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Flawless.Core.Modal; - -namespace Flawless.Core.Interface; - -public interface IRepositoryCommit -{ - public IReadonlyRepository Repository { get; } - - public UInt64 CommitId { get; } - - public Author Author { get; } - - public DateTime CommitTime { get; } - - public string Message { get; } - - public ulong ManifestId { get; } - - public IRepositoryCommit? GetParentCommit(); - - public IRepositoryCommit? GetChildCommit(); -} \ No newline at end of file diff --git a/Flawless.Core/Modal/Author.cs b/Flawless.Core/Modal/Author.cs index c4f568a..fe302aa 100644 --- a/Flawless.Core/Modal/Author.cs +++ b/Flawless.Core/Modal/Author.cs @@ -4,4 +4,4 @@ /// An author setup to indicate who create a depot or identify a depot author when uploading it. /// [Serializable] -public record struct Author(string Name, string Email); \ No newline at end of file +public record struct Author(string Name, Guid Email); \ No newline at end of file diff --git a/Flawless.Core/Modal/CommitManifest.cs b/Flawless.Core/Modal/CommitManifest.cs index e7fd338..e172b58 100644 --- a/Flawless.Core/Modal/CommitManifest.cs +++ b/Flawless.Core/Modal/CommitManifest.cs @@ -1,4 +1,4 @@ namespace Flawless.Core.Modal; [Serializable] -public record struct CommitManifest(ulong ManifestId, DepotLabel Depot, string[] FilePaths); \ No newline at end of file +public record struct CommitManifest(Guid ManifestId, DepotLabel Depot, string[] FilePaths); \ No newline at end of file diff --git a/Flawless.Core/Modal/DepotLabel.cs b/Flawless.Core/Modal/DepotLabel.cs index 1f42ced..5c4866d 100644 --- a/Flawless.Core/Modal/DepotLabel.cs +++ b/Flawless.Core/Modal/DepotLabel.cs @@ -3,4 +3,4 @@ namespace Flawless.Core.Modal; [Serializable] -public record struct DepotLabel(HashId Id, HashId[] Baselines); \ No newline at end of file +public record struct DepotLabel(HashId Id, HashId[] Dependency); \ No newline at end of file diff --git a/Flawless.Server/Controllers/AdminUserController.cs b/Flawless.Server/Controllers/AdminUserController.cs index 2c71f89..bd3d385 100644 --- a/Flawless.Server/Controllers/AdminUserController.cs +++ b/Flawless.Server/Controllers/AdminUserController.cs @@ -11,11 +11,10 @@ namespace Flawless.Server.Controllers; public class AdminUserController( UserManager userManager) : ControllerBase { - [HttpPost("user/delete")] - public async Task DeleteUserAsync(LocateUserRequest r) + [HttpPost("user/delete/{username}")] + public async Task DeleteUserAsync(string username) { - if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!")); - var user = await userManager.FindByIdAsync(r.UserId); + var user = await userManager.FindByNameAsync(username); if (user == null) return BadRequest(new FailedResponse("User does not exist!")); var result = await userManager.DeleteAsync(user); @@ -24,11 +23,10 @@ public class AdminUserController( return Ok(); } - [HttpPost("user/enable")] - public async Task EnableUserAsync(LocateUserRequest r) + [HttpPost("user/enable/{username}")] + public async Task EnableUserAsync(string username) { - if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!")); - var user = await userManager.FindByIdAsync(r.UserId); + var user = await userManager.FindByNameAsync(username); if (user == null) return BadRequest(new FailedResponse("User does not exist!")); var result = await userManager.SetLockoutEnabledAsync(user, false); @@ -37,11 +35,10 @@ public class AdminUserController( return Ok(); } - [HttpPost("user/disable")] - public async Task DisableUserAsync(LocateUserRequest r) + [HttpPost("user/disable/{username}")] + public async Task DisableUserAsync(string username) { - if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!")); - var user = await userManager.FindByIdAsync(r.UserId); + var user = await userManager.FindByNameAsync(username); if (user == null) return BadRequest(new FailedResponse("User does not exist!")); var result = await userManager.SetLockoutEnabledAsync(user, true); diff --git a/Flawless.Server/Controllers/RepositoryControl.cs b/Flawless.Server/Controllers/RepositoryControl.cs new file mode 100644 index 0000000..f4db26e --- /dev/null +++ b/Flawless.Server/Controllers/RepositoryControl.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Flawless.Server.Controllers; + +[ApiController, Authorize, Route("api/repo/{userName}/{repositoryName}")] +public class RepositoryControl : ControllerBase +{ + +} \ No newline at end of file diff --git a/Flawless.Server/Controllers/RepositoryController.cs b/Flawless.Server/Controllers/RepositoryController.cs deleted file mode 100644 index e734d5d..0000000 --- a/Flawless.Server/Controllers/RepositoryController.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Flawless.Server.Controllers; - -[ApiController, Route("api/repository")] -public class RepositoryController : ControllerBase -{ - -} \ No newline at end of file diff --git a/Flawless.Server/Controllers/RepositoryManageController.cs b/Flawless.Server/Controllers/RepositoryManageController.cs new file mode 100644 index 0000000..641f6f9 --- /dev/null +++ b/Flawless.Server/Controllers/RepositoryManageController.cs @@ -0,0 +1,232 @@ +using Flawless.Communication.Request; +using Flawless.Communication.Response; +using Flawless.Communication.Shared; +using Flawless.Server.Models; +using Flawless.Server.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Flawless.Server.Controllers; + +[ApiController, Authorize, Route("api/repo_manage")] +public class RepositoryManageController(AppDbContext dbContext, UserManager userManager) : ControllerBase +{ + [HttpGet("list")] + public async Task ListAllAvailableRepositoriesAsync(QueryPagesRequest r) + { + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var query = await dbContext.Repositories + .Include(repository => repository.Owner) + .Include(repository => repository.Commits) + .Include(repository => repository.Members) + .ThenInclude(repositoryMember => repositoryMember.User) + .Where(rp => rp.Members.Any(m => m.User == u)) + .Skip(r.Offset) + .Take(r.Length) + .ToArrayAsync(); + + return Ok(new PagedResponse + { + Length = r.Length, + Offset = r.Offset, + Data = query.Select(rp => new RepositoryInfoResponse + { + RepositoryName = rp.Name, + OwnerUsername = rp.Owner.UserName!, + LatestCommitId = rp.Commits.OrderByDescending(cm => cm.CommittedOn).FirstOrDefault()?.Id ?? Guid.Empty, + Description = rp.Description, + IsArchived = rp.IsArchived, + Role = rp.Members.First(m => m.User == u).Role + }), + }); + } + + [HttpPost("create/{repositoryName}")] + public async Task CreateRepositoryAsync(string repositoryName) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + if (await dbContext.Repositories.AnyAsync(rp => rp.Name == repositoryName && u == rp.Owner)) + return BadRequest(new FailedResponse("Repository name has already created!")); + + await dbContext.Repositories.AddAsync(new Repository() + { + Name = repositoryName, + Owner = u, + }); + + await dbContext.SaveChangesAsync(); + return Ok(); + } + + [HttpPost("delete/{repositoryName}")] + public async Task DeleteRepositoryAsync(string repositoryName) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + + dbContext.Repositories.Remove(rp); + await dbContext.SaveChangesAsync(); + return Ok(); + } + + [HttpGet("info/{repositoryName}")] + public async Task IsRepositoryArchiveAsync(string repositoryName) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var rp = await dbContext.Repositories + .Include(repository => repository.Owner) + .Include(repository => repository.Commits) + .Include(repository => repository.Members) + .ThenInclude(repositoryMember => repositoryMember.User) + .FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + + return Ok(new RepositoryInfoResponse + { + RepositoryName = rp.Name, + OwnerUsername = rp.Owner.UserName!, + LatestCommitId = rp.Commits.Max(cm => cm.Id), + Description = rp.Description, + IsArchived = rp.IsArchived, + Role = rp.Members.First(m => m.User == u).Role + }); + } + + [HttpPost("archive/{repositoryName}")] + public async Task ArchiveRepositoryAsync(string repositoryName) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + if (rp.IsArchived) return BadRequest(new FailedResponse("Repository is archived!")); + + rp.IsArchived = true; + await dbContext.SaveChangesAsync(); + return Ok(); + } + + + [HttpPost("unarchive/{repositoryName}")] + public async Task UnarchiveRepositoryAsync(string repositoryName) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + if (!rp.IsArchived) return BadRequest(new FailedResponse("Repository is not archived!")); + + rp.IsArchived = false; + await dbContext.SaveChangesAsync(); + return Ok(); + } + + [HttpGet("get_users/{repositoryName}")] + public async Task GetUsersAsync(string repositoryName, QueryPagesRequest r) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var rp = await dbContext.Repositories + .Include(repository => repository.Owner) + .Include(repository => repository.Members) + .ThenInclude(repositoryMember => repositoryMember.User) + .FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + + return Ok(new PagedResponse + { + Length = r.Length, + Offset = r.Offset, + Total = rp.Members.Count, + Data = rp.Members.Select(pm => new RepoUserRole + { + Username = pm.User.UserName!, + Role = pm.Role + }) + }); + } + + [HttpPost("update_user/{repositoryName}")] + public async Task UpdateUserAsync(string repositoryName, [FromBody] RepoUserRole r) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var tu = await userManager.FindByNameAsync(r.Username); + if (tu == null) return BadRequest(new FailedResponse("User not found!")); + if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!")); + + var rp = await dbContext.Repositories + .Include(repository => repository.Members) + .ThenInclude(repositoryMember => repositoryMember.User) + .FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + + var m = rp.Members.FirstOrDefault(m => m.User == tu); + if (m == null) + { + m = new RepositoryMember + { + User = tu, + Role = r.Role ?? RepositoryRole.Guest + }; + + rp.Members.Add(m); + } + else + { + m.Role = r.Role ?? RepositoryRole.Guest; + } + + await dbContext.SaveChangesAsync(); + return Ok(); + } + + [HttpPost("delete_user/{repositoryName}")] + public async Task DeleteUserAsync(string repositoryName, [FromBody] RepoUserRole r) + { + if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3) + return BadRequest(new FailedResponse("Repository name is empty or too short!")); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + var tu = await userManager.FindByNameAsync(r.Username); + if (tu == null) return BadRequest(new FailedResponse("User not found!")); + if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!")); + + var rp = await dbContext.Repositories + .Include(repository => repository.Members) + .ThenInclude(repositoryMember => repositoryMember.User) + .FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); + + if (rp == null) return BadRequest(new FailedResponse("Repository not found!")); + + var m = rp.Members.FirstOrDefault(m => m.User == tu); + if (m == null) return BadRequest(new FailedResponse("User is not being granted to this repository!")); + + rp.Members.Remove(m); + await dbContext.SaveChangesAsync(); + return Ok(); + } +} \ No newline at end of file diff --git a/Flawless.Server/Controllers/UserController.cs b/Flawless.Server/Controllers/UserController.cs index 3bb03a2..f920560 100644 --- a/Flawless.Server/Controllers/UserController.cs +++ b/Flawless.Server/Controllers/UserController.cs @@ -81,38 +81,31 @@ public class UserController( return Ok(); } - [HttpGet("get/info")] - public async Task> GetUserInfoAsync(LocateUserRequest r) + [HttpGet("get")] + public async Task> GetUserInfoAsync() { var self = (await userManager.GetUserAsync(HttpContext.User))!; - - if (r.UserId != null) - { - var u = await userManager.FindByIdAsync(r.UserId); - if (u == null) return BadRequest(new FailedResponse("User is not existed!")); - - return Ok(GetUserInfoInternal(u, self)); - } - - if (r.Username != null) - { - var u = await userManager.FindByNameAsync(r.Username); - if (u == null) return BadRequest(new FailedResponse("User is not existed!")); - - return Ok(GetUserInfoInternal(u, self)); - } // Return self as default return Ok(GetUserInfoInternal(self, self)); } - [HttpGet("query/info")] - public async Task>> GetUserInfoAsync(QueryPagesRequest r) + [HttpGet("get/{username}")] + public async Task> GetUserInfoAsync(string username) + { + var self = (await userManager.GetUserAsync(HttpContext.User))!; + + var u = await userManager.FindByNameAsync(username); + if (u == null) return BadRequest(new FailedResponse("User is not existed!")); + + return Ok(GetUserInfoInternal(u, self)); + } + + [HttpGet("query/{keyword}")] + public async Task>> GetUserInfoAsync(QueryPagesRequest r, string keyword) { - var queryNamePrefix = r.Parameter?.Username ?? String.Empty; - var queryId = r.Parameter == null ? Guid.Empty : Guid.Parse(r.Parameter.UserId!); var payload = await userManager.Users - .Where(u => u.UserName!.StartsWith(queryNamePrefix) || u.Id == queryId) + .Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword))) .Skip(r.Offset) .Take(r.Length) .Select(u => GetUserInfoInternal(u, null)) @@ -141,12 +134,15 @@ public class UserController( var authorized = queryUser.Id == currentUser?.Id; return new UserInfoResponse { + Authorized = authorized, Username = queryUser.UserName, CreatedAt = queryUser.CreatedOn, Bio = queryUser.Bio, Gender = queryUser.Gender, NickName = queryUser.NickName, + PublicEmail = authorized ? queryUser.PublicEmail : null, Email = queryUser.PublicEmail || authorized ? queryUser.Email : null, + Phone = authorized ? queryUser.PhoneNumber : null, }; } } \ No newline at end of file diff --git a/Flawless.Server/Controllers/WebSocketTransferController.cs b/Flawless.Server/Controllers/WebSocketTransferController.cs index 1c4d24d..3a9a5f6 100644 --- a/Flawless.Server/Controllers/WebSocketTransferController.cs +++ b/Flawless.Server/Controllers/WebSocketTransferController.cs @@ -1,19 +1,46 @@ -using Microsoft.AspNetCore.Mvc; +using Flawless.Server.Models; +using Flawless.Server.Services; +using Flawless.Server.Utility; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; namespace Flawless.Server.Controllers; -[ApiController] -[Route("ws/transfer")] -public class WebSocketTransferController : ControllerBase +[ApiController, Authorize, Route("ws/transfer")] +public class WebSocketTransferController( + PathTransformer transformer, + RepoTransmissionContext transmission, + UserManager userManager) : ControllerBase { - [Route("download")] - public async Task DownloadDepotAsync([FromHeader] string resource, [FromHeader] string? validate) - { - } - - [Route("upload")] - public async Task UploadDepotAsync([FromHeader] string resource, [FromHeader] string? validate) + [Route("download/{taskId}")] + public async Task DownloadDepotAsync(string taskId) { + var (u, task) = await GetTaskAsync(transmission.DownloadTask, taskId); } + + [Route("upload/{taskId}")] + public async Task UploadDepotAsync(string taskId) + { + var (u, task) = await GetTaskAsync(transmission.DownloadTask, taskId); + + } + + private async ValueTask<(AppUser user, RepoDepotTransmissionTask task)> GetTaskAsync + (IDictionary taskList, string taskId) + { + if (!Guid.TryParse(taskId, out var id)) + throw new ArgumentException("Not a valid task id!", nameof(taskId)); + + if (!taskList.TryGetValue(id, out var task)) + throw new ArgumentException("Task not found.", nameof(taskId)); + + var u = (await userManager.GetUserAsync(HttpContext.User))!; + if (task.UserId != u.Id) throw new ArgumentException("Not being authorized!", nameof(taskId)); + if (DateTime.UtcNow > task.ExpiresOn) + throw new ArgumentException("Task is expired!", nameof(taskId)); + + return (u, task); + } } \ No newline at end of file diff --git a/Flawless.Server/Models/AppUser.cs b/Flawless.Server/Models/AppUser.cs index 5aee95d..c7b4180 100644 --- a/Flawless.Server/Models/AppUser.cs +++ b/Flawless.Server/Models/AppUser.cs @@ -7,6 +7,7 @@ namespace Flawless.Server.Models; public class AppUser : IdentityUser { + [Required] public DateTime CreatedOn { get; set; } public UserSex Gender { get; set; } @@ -16,8 +17,9 @@ public class AppUser : IdentityUser [MaxLength(200)] public string? Bio { get; set; } - - public bool PublicEmail { get; set; } + + [Required] + public bool PublicEmail { get; set; } = true; public void RenewSecurityStamp() diff --git a/Flawless.Server/Models/AppUserRefreshKey.cs b/Flawless.Server/Models/AppUserRefreshKey.cs index 5e00a62..f329cda 100644 --- a/Flawless.Server/Models/AppUserRefreshKey.cs +++ b/Flawless.Server/Models/AppUserRefreshKey.cs @@ -5,9 +5,12 @@ namespace Flawless.Server.Models; public class AppUserRefreshKey { [Key] + [Required] public required string RefreshToken { get; set; } - public Guid UserId { get; set; } + [Required] + public required Guid UserId { get; set; } - public DateTime ExpireIn { get; set; } + [Required] + public required DateTime ExpireIn { get; set; } } \ No newline at end of file diff --git a/Flawless.Server/Models/RepoDepotTransmissionTask.cs b/Flawless.Server/Models/RepoDepotTransmissionTask.cs new file mode 100644 index 0000000..586c963 --- /dev/null +++ b/Flawless.Server/Models/RepoDepotTransmissionTask.cs @@ -0,0 +1,12 @@ +namespace Flawless.Server.Models; + +public class RepoDepotTransmissionTask +{ + public required Guid UserId { get; set; } + + public required Guid RepositoryId { get; set; } + + public required Guid DepotId { get; set; } + + public required DateTime ExpiresOn { get; set; } +} \ No newline at end of file diff --git a/Flawless.Server/Models/Repository.cs b/Flawless.Server/Models/Repository.cs new file mode 100644 index 0000000..433ed22 --- /dev/null +++ b/Flawless.Server/Models/Repository.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace Flawless.Server.Models; + +public class Repository +{ + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required AppUser Owner { get; set; } + + [Required] + public required string Name { get; set; } + + public string? Description { get; set; } + + [Required] + public bool IsArchived { get; set; } = false; + + public List Members { get; set; } = new(); + + public List Commits { get; set; } = new(); +} \ No newline at end of file diff --git a/Flawless.Server/Models/RepositoryCommit.cs b/Flawless.Server/Models/RepositoryCommit.cs new file mode 100644 index 0000000..7490dbd --- /dev/null +++ b/Flawless.Server/Models/RepositoryCommit.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Flawless.Server.Models; + +public class RepositoryCommit +{ + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required DateTime CommittedOn { get; set; } + + [Required] + public string Message { get; set; } = String.Empty; + + [Required] + public required RepositoryDepot MainDepot { get; set; } + + public RepositoryCommit? Parent { get; set; } +} \ No newline at end of file diff --git a/Flawless.Server/Models/RepositoryDepot.cs b/Flawless.Server/Models/RepositoryDepot.cs new file mode 100644 index 0000000..53c55e5 --- /dev/null +++ b/Flawless.Server/Models/RepositoryDepot.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Flawless.Server.Models; + +public class RepositoryDepot +{ + [Key] + [Required] + public Guid DepotId { get; set; } + + [Required] + public List Dependencies { get; set; } = new(); +} \ No newline at end of file diff --git a/Flawless.Server/Models/RepositoryMember.cs b/Flawless.Server/Models/RepositoryMember.cs new file mode 100644 index 0000000..0612bf0 --- /dev/null +++ b/Flawless.Server/Models/RepositoryMember.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Flawless.Communication.Shared; + +namespace Flawless.Server.Models; + +public class RepositoryMember +{ + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required AppUser User { get; set; } + + [Required] + public RepositoryRole Role { get; set; } = RepositoryRole.Guest; +} \ No newline at end of file diff --git a/Flawless.Server/Program.cs b/Flawless.Server/Program.cs index bb062aa..e97f7f7 100644 --- a/Flawless.Server/Program.cs +++ b/Flawless.Server/Program.cs @@ -5,6 +5,7 @@ using Flawless.Communication.Response; using Flawless.Server.Middlewares; using Flawless.Server.Models; using Flawless.Server.Services; +using Flawless.Server.Utility; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -33,6 +34,8 @@ public static class Program private static void ConfigAppService(WebApplicationBuilder builder) { // Api related + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddOpenApi(); builder.Services.AddControllers() .AddJsonOptions(opt => @@ -50,7 +53,6 @@ public static class Program private static void ConfigDbContext(WebApplicationBuilder builder) { // Data connection related. - builder.Services.AddDbContextFactory(); builder.Services.AddDbContext(opt => { opt.UseNpgsql(builder.Configuration.GetConnectionString("CoreDb")); diff --git a/Flawless.Server/Services/AppDbContext.cs b/Flawless.Server/Services/AppDbContext.cs index eae992e..1f0018c 100644 --- a/Flawless.Server/Services/AppDbContext.cs +++ b/Flawless.Server/Services/AppDbContext.cs @@ -10,4 +10,5 @@ public class AppDbContext(DbContextOptions options) { public DbSet RefreshTokens { get; set; } + public DbSet Repositories { get; set; } } \ No newline at end of file diff --git a/Flawless.Server/Services/RepoTransmissionContext.cs b/Flawless.Server/Services/RepoTransmissionContext.cs new file mode 100644 index 0000000..ab6c417 --- /dev/null +++ b/Flawless.Server/Services/RepoTransmissionContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; +using Flawless.Server.Models; + +namespace Flawless.Server.Services; + +public class RepoTransmissionContext +{ + private readonly ConcurrentDictionary _uploadTask = new(); + + private readonly ConcurrentDictionary _downloadTask = new(); + + public IDictionary UploadTask => _uploadTask; + + public IDictionary DownloadTask => _downloadTask; +} \ No newline at end of file diff --git a/Flawless.Server/Services/RepositoryContext.cs b/Flawless.Server/Services/RepositoryContext.cs deleted file mode 100644 index 8917267..0000000 --- a/Flawless.Server/Services/RepositoryContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Flawless.Server.Services; - -public class RepositoryContext : DbContext -{ - private readonly string RepositoryPath; - - public RepositoryContext(IConfiguration config, DbContextOptions options) : base(options) - { - RepositoryPath = Path.Combine(config["LocalStoragePath"] ?? "./Data", "Repository"); - - if (!Directory.Exists(RepositoryPath)) - Directory.CreateDirectory(RepositoryPath); - } - -} \ No newline at end of file diff --git a/Flawless.Server/Services/RepositoryContextFactory.cs b/Flawless.Server/Services/RepositoryContextFactory.cs deleted file mode 100644 index 54e0edf..0000000 --- a/Flawless.Server/Services/RepositoryContextFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; - -namespace Flawless.Server.Services; - -public class RepositoryContextFactory : DbContextFactory -{ - public RepositoryContextFactory(IServiceProvider serviceProvider, DbContextOptions options, IDbContextFactorySource factorySource) - : base(serviceProvider, options, factorySource) - { - } -} \ No newline at end of file diff --git a/Flawless.Server/Utility/PathTransformer.cs b/Flawless.Server/Utility/PathTransformer.cs new file mode 100644 index 0000000..2fd0611 --- /dev/null +++ b/Flawless.Server/Utility/PathTransformer.cs @@ -0,0 +1,34 @@ +namespace Flawless.Server.Utility; + +public class PathTransformer(IConfiguration config) +{ + + private readonly string _rootPath = config.GetValue("LocalStoragePath") ?? "./Storage"; + + public string GetLocalStoragePath() + { + return _rootPath; + } + + public string GetRepositoryRootPath(Guid repositoryId) + { + if (repositoryId == Guid.Empty) throw new ArgumentException(nameof(repositoryId)); + return Path.Combine(_rootPath, repositoryId.ToString()); + } + + public string GetCommitManifest(Guid repositoryId, Guid commitId) + { + if (repositoryId == Guid.Empty) throw new ArgumentException(nameof(repositoryId)); + if (commitId == Guid.Empty) throw new ArgumentException(nameof(commitId)); + + return Path.Combine(_rootPath, repositoryId.ToString(), commitId.ToString()); + } + + public string GetDepotManifest(Guid repositoryId, Guid depotId) + { + if (repositoryId == Guid.Empty) throw new ArgumentException(nameof(repositoryId)); + if (depotId == Guid.Empty) throw new ArgumentException(nameof(depotId)); + + return Path.Combine(_rootPath, repositoryId.ToString(), depotId.ToString()); + } +} \ No newline at end of file