diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..41c2068 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "nswag.consolecore": { + "version": "14.2.0", + "commands": [ + "nswag" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml index caa4d57..8859eb9 100644 --- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml +++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml @@ -4,6 +4,8 @@ diff --git a/Flawless-Version-Control.sln b/Flawless-Version-Control.sln index 3b01626..5c7fc60 100644 --- a/Flawless-Version-Control.sln +++ b/Flawless-Version-Control.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Communication", "F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Server", "Flawless.Server\Flawless.Server.csproj", "{66142212-034C-4702-92FE-5C625D725048}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flawless.Client", "Flawless.Client\Flawless.Client.csproj", "{CEC2183E-0097-4972-BBEB-7CE3C6D874B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {66142212-034C-4702-92FE-5C625D725048}.Debug|Any CPU.Build.0 = Debug|Any CPU {66142212-034C-4702-92FE-5C625D725048}.Release|Any CPU.ActiveCfg = Release|Any CPU {66142212-034C-4702-92FE-5C625D725048}.Release|Any CPU.Build.0 = Release|Any CPU + {CEC2183E-0097-4972-BBEB-7CE3C6D874B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEC2183E-0097-4972-BBEB-7CE3C6D874B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEC2183E-0097-4972-BBEB-7CE3C6D874B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEC2183E-0097-4972-BBEB-7CE3C6D874B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user index 7c73cf0..739c965 100644 --- a/Flawless-Version-Control.sln.DotSettings.user +++ b/Flawless-Version-Control.sln.DotSettings.user @@ -1,5 +1,6 @@  ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Flawless.Client/App.axaml b/Flawless.Client/App.axaml new file mode 100644 index 0000000..a17c221 --- /dev/null +++ b/Flawless.Client/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Flawless.Client/App.axaml.cs b/Flawless.Client/App.axaml.cs new file mode 100644 index 0000000..8889aed --- /dev/null +++ b/Flawless.Client/App.axaml.cs @@ -0,0 +1,40 @@ +using System; +using System.Net.Http; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Flawless.Client.Remote; +using Flawless.Client.ViewModels; +using Flawless.Client.Views; +using Refit; + +namespace Flawless.Client; + +public partial class App : Application +{ + public IFlawlessServer ApiGateway { get; private set; } + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + ApiGateway = RestService.For(new HttpClient(new AuthHeaderHandler()) + { + BaseAddress = new Uri("http://localhost:5256/"), + Timeout = TimeSpan.FromSeconds(60) + }); + } + + public override void OnFrameworkInitializationCompleted() + { + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Flawless.Client/Assets/avalonia-logo.ico b/Flawless.Client/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/Flawless.Client/Assets/avalonia-logo.ico differ diff --git a/Flawless.Client/AuthHeaderHandler.cs b/Flawless.Client/AuthHeaderHandler.cs new file mode 100644 index 0000000..b506429 --- /dev/null +++ b/Flawless.Client/AuthHeaderHandler.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Flawless.Client.Remote; + +namespace Flawless.Client; + +public class AuthHeaderHandler : DelegatingHandler +{ + private string? AuthenticationHeader { get; set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = await SendCommandAsync(request, cancellationToken); + var retryCount = 0; + + while (response.Headers.TryGetValues("Token-Expired", out var expired) && expired.Any(s => s == "true")) + { + if (retryCount++ > 3) + { + AuthenticationHeader = null; + throw new TimeoutException("Too many retries, login info was cleared"); + } + + var refreshRequest = new HttpRequestMessage(HttpMethod.Post, "api/auth/refresh"); + var refresh = await base.SendAsync(refreshRequest, cancellationToken); + if (!response.IsSuccessStatusCode) throw new ApplicationException("Login is expired and require login!"); + + await using var st = await refresh.Content.ReadAsStreamAsync(cancellationToken); + var tk = await JsonSerializer.DeserializeAsync(st, cancellationToken: cancellationToken) + ?? throw new ApplicationException("Not able to refresh token, please login again!"); + + AuthenticationHeader = tk.Token; + response = await SendCommandAsync(request, cancellationToken); + } + + return response; + } + + private Task SendCommandAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Prefill this header + if (AuthenticationHeader != null) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationHeader); + + return base.SendAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj new file mode 100644 index 0000000..3609a1b --- /dev/null +++ b/Flawless.Client/Flawless.Client.csproj @@ -0,0 +1,36 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + + + None + All + + + + + + + + + + + diff --git a/Flawless.Client/Program.cs b/Flawless.Client/Program.cs new file mode 100644 index 0000000..af0733a --- /dev/null +++ b/Flawless.Client/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.ReactiveUI; +using System; + +namespace Flawless.Client; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} \ No newline at end of file diff --git a/Flawless.Client/Service/Remote_Generated.cs b/Flawless.Client/Service/Remote_Generated.cs new file mode 100644 index 0000000..67d5579 --- /dev/null +++ b/Flawless.Client/Service/Remote_Generated.cs @@ -0,0 +1,458 @@ +// +// This code was generated by Refitter. +// + + +using Refit; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable annotations + +namespace Flawless.Client.Remote +{ + [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.5.2.0")] + public partial interface IFlawlessServer + { + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/admin/user/delete/{username}")] + Task Delete(string username, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/admin/user/enable/{username}")] + Task Enable(string username, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/admin/user/disable/{username}")] + Task Disable(string username, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/admin/user/reset_password")] + Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/auth/register")] + Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default); + + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] + [Post("/api/auth/login")] + Task Login([Body] LoginRequest body, CancellationToken cancellationToken = default); + + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] + [Post("/api/auth/refresh")] + Task Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/auth/logout_all")] + Task LogoutAll(CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/auth/renew_password")] + Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/")] + Task Index(CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/delete_repo")] + Task DeleteRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/get_info")] + Task GetInfo(string repositoryName, string userName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/archive_repo")] + Task ArchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")] + Task UnarchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/get_users")] + Task GetUsers(string repositoryName, string userName, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/update_user")] + Task UpdateUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/delete_user")] + Task DeleteUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/fetch_manifest")] + Task FetchManifest(string userName, string repositoryName, [Query] string commitId, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/fetch_depot")] + Task FetchDepot(string userName, string repositoryName, [Query] string depotId, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/list_commit")] + Task ListCommit(string userName, string repositoryName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/list_locked_files")] + Task ListLockedFiles(string userName, string repositoryName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo/{userName}/{repositoryName}/peek_commit")] + Task PeekCommit(string userName, string repositoryName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/lock_file")] + Task LockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo/{userName}/{repositoryName}/unlock_file")] + Task UnlockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Multipart] + [Post("/api/repo/{userName}/{repositoryName}/create_commit")] + Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable workspaceSnapshot, IEnumerable requiredDepots, string mainDepotId, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/repo_list")] + Task RepoList([Query, AliasAs("Offset")] int offset, [Query, AliasAs("Length")] int length, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/repo_create")] + Task RepoCreate([Query] string repositoryName, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/user/update_info")] + Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/user/update_email")] + Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Post("/api/user/update_phone")] + Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default); + + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/user/get_info")] + Task GetInfo([Query] string username, CancellationToken cancellationToken = default); + + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/user/query_info")] + Task QueryInfo([Query] string keyword, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default); + + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Get("/api/user/delete")] + Task Delete(CancellationToken cancellationToken = default); + + + } + +} + +//---------------------- +// +// Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Flawless.Client.Remote +{ + using System = global::System; + + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LoginRequest + { + + [JsonPropertyName("username")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Username { get; set; } + + [JsonPropertyName("password")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Password { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class QueryPagesRequest + { + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("length")] + public int Length { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RegisterRequest + { + + [JsonPropertyName("email")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Email { get; set; } + + [JsonPropertyName("username")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Username { get; set; } + + [JsonPropertyName("password")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Password { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepoUserRole + { + + [JsonPropertyName("username")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Username { get; set; } + + [JsonPropertyName("role")] + public RepositoryRole Role { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum RepositoryRole + { + + _0 = 0, + + _1 = 1, + + _2 = 2, + + _3 = 3, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ResetPasswordRequest + { + + [JsonPropertyName("identity")] + public string Identity { get; set; } + + [JsonPropertyName("oldPassword")] + public string OldPassword { get; set; } + + [JsonPropertyName("newPassword")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string NewPassword { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class TokenInfo + { + + [JsonPropertyName("token")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Token { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserContactModifyResponse + { + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("phone")] + public string Phone { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserInfoModifyResponse + { + + [JsonPropertyName("nickName")] + public string NickName { get; set; } + + [JsonPropertyName("gender")] + public UserSex Gender { get; set; } + + [JsonPropertyName("bio")] + public string Bio { get; set; } + + [JsonPropertyName("publicEmail")] + public bool? PublicEmail { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserInfoResponse + { + + [JsonPropertyName("authorized")] + public bool Authorized { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("nickName")] + public string NickName { get; set; } + + [JsonPropertyName("gender")] + public UserSex Gender { get; set; } + + [JsonPropertyName("bio")] + public string Bio { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("phone")] + public string Phone { get; set; } + + [JsonPropertyName("publicEmail")] + public bool? PublicEmail { get; set; } + + [JsonPropertyName("createdAt")] + public System.DateTimeOffset? CreatedAt { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserInfoResponsePagedResponse + { + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("length")] + public int Length { get; set; } + + [JsonPropertyName("total")] + public int? Total { get; set; } + + [JsonPropertyName("data")] + public ICollection Data { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum UserSex + { + + _0 = 0, + + _1 = 1, + + _2 = 2, + + _3 = 3, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class WorkspaceFile + { + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FileParameter + { + public FileParameter(System.IO.Stream data) + : this (data, null, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName) + : this (data, fileName, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName, string contentType) + { + Data = data; + FileName = fileName; + ContentType = contentType; + } + + public System.IO.Stream Data { get; private set; } + + public string FileName { get; private set; } + + public string ContentType { get; private set; } + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/Flawless.Client/ViewLocator.cs b/Flawless.Client/ViewLocator.cs new file mode 100644 index 0000000..39a0d91 --- /dev/null +++ b/Flawless.Client/ViewLocator.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Flawless.Client.ViewModels; + +namespace Flawless.Client; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/MainWindowViewModel.cs b/Flawless.Client/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..a8791f5 --- /dev/null +++ b/Flawless.Client/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,6 @@ +namespace Flawless.Client.ViewModels; + +public class MainWindowViewModel : ViewModelBase +{ + public string Greeting { get; } = "Welcome to Avalonia!"; +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ViewModelBase.cs b/Flawless.Client/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..1d53eb3 --- /dev/null +++ b/Flawless.Client/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using ReactiveUI; + +namespace Flawless.Client.ViewModels; + +public class ViewModelBase : ReactiveObject +{ +} \ No newline at end of file diff --git a/Flawless.Client/Views/MainWindow.axaml b/Flawless.Client/Views/MainWindow.axaml new file mode 100644 index 0000000..af84898 --- /dev/null +++ b/Flawless.Client/Views/MainWindow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/Flawless.Client/Views/MainWindow.axaml.cs b/Flawless.Client/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..4ff525d --- /dev/null +++ b/Flawless.Client/Views/MainWindow.axaml.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using Avalonia.Controls; +using Flawless.Client.Remote; + +namespace Flawless.Client.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + Test(); + } + + private async void Test() + { + var result = await (App.Current as App).ApiGateway.Login(new LoginRequest + { + Username = "cardidi", + Password = "8888" + }, CancellationToken.None); + + Console.WriteLine(result); + } +} \ No newline at end of file diff --git a/Flawless.Client/app.manifest b/Flawless.Client/app.manifest new file mode 100644 index 0000000..f38423a --- /dev/null +++ b/Flawless.Client/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/Flawless.Server/Controllers/AuthenticationController.cs b/Flawless.Server/Controllers/AuthenticationController.cs index cd2c93c..1312e20 100644 --- a/Flawless.Server/Controllers/AuthenticationController.cs +++ b/Flawless.Server/Controllers/AuthenticationController.cs @@ -50,7 +50,7 @@ public class AuthenticationController( } [HttpPost("login")] - public async Task LoginAsync(LoginRequest r) + public async Task> LoginAsync(LoginRequest r) { var user = await userManager.FindByNameAsync(r.Username); if (user == null) return BadRequest(new FailedResponse("Invalid username or password.")); diff --git a/Flawless.Server/Controllers/RepositoryControl.cs b/Flawless.Server/Controllers/RepositoryInnieController.cs similarity index 67% rename from Flawless.Server/Controllers/RepositoryControl.cs rename to Flawless.Server/Controllers/RepositoryInnieController.cs index dce3c88..2abad88 100644 --- a/Flawless.Server/Controllers/RepositoryControl.cs +++ b/Flawless.Server/Controllers/RepositoryInnieController.cs @@ -17,7 +17,7 @@ using Microsoft.EntityFrameworkCore; namespace Flawless.Server.Controllers; [ApiController, Authorize, Route("api/repo/{userName}/{repositoryName}")] -public class RepositoryControl( +public class RepositoryInnieController( UserManager userManager, AppDbContext dbContext, PathTransformer transformer, @@ -30,6 +30,178 @@ public class RepositoryControl( } + #region Unresoted + + [HttpPost("delete_repo")] + 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("get_info")] + 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_repo")] + 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_repo")] + 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")] + 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")] + 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")] + 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(); + } + + #endregion + + private bool UserNotGranted(out IActionResult? rsp, AppUser user, Repository repo, RepositoryRole minRole) { if (repo.Owner == user || repo.Members.Any(m => m.User == user && m.Role >= minRole)) @@ -57,8 +229,8 @@ public class RepositoryControl( } - [HttpGet("fetch/manifest/{commitId}")] - public async Task DownloadManifestAsync(string userName, string repositoryName, string commitId) + [HttpGet("fetch_manifest")] + public async Task DownloadManifestAsync(string userName, string repositoryName, [FromQuery] string commitId) { if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id")); var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -72,8 +244,8 @@ public class RepositoryControl( } - [HttpGet("fetch/depot/{depotId}")] - public async Task DownloadDepotAsync(string userName, string repositoryName, string depotId) + [HttpGet("fetch_depot")] + public async Task DownloadDepotAsync(string userName, string repositoryName, [FromQuery] string depotId) { if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id")); var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -87,7 +259,7 @@ public class RepositoryControl( } - [HttpGet("list/commit")] + [HttpGet("list_commit")] public async Task ListCommitsAsync(string userName, string repositoryName) { var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -107,7 +279,7 @@ public class RepositoryControl( }); } - [HttpGet("list/locked")] + [HttpGet("list_locked_files")] public async Task ListLocksAsync(string userName, string repositoryName) { var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -126,7 +298,7 @@ public class RepositoryControl( }); } - [HttpGet("peek/commit")] + [HttpGet("peek_commit")] public async Task PeekCommitAsync(string userName, string repositoryName) { var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -147,7 +319,7 @@ public class RepositoryControl( } - [HttpGet("lock")] + [HttpPost("lock_file")] public async Task LockAsync(string userName, string repositoryName, string path) { var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -184,7 +356,7 @@ public class RepositoryControl( return BadRequest("Unknown error"); } - [HttpGet("unlock")] + [HttpPost("unlock_file")] public async Task UnockAsync(string userName, string repositoryName, string path) { var user = (await userManager.GetUserAsync(HttpContext.User))!; @@ -221,7 +393,7 @@ public class RepositoryControl( return BadRequest("Unknown error"); } - [HttpPost("commit")] + [HttpPost("create_commit")] public async Task CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req) { var user = (await userManager.GetUserAsync(HttpContext.User))!; diff --git a/Flawless.Server/Controllers/RepositoryManageController.cs b/Flawless.Server/Controllers/RepositoryManageController.cs deleted file mode 100644 index 641f6f9..0000000 --- a/Flawless.Server/Controllers/RepositoryManageController.cs +++ /dev/null @@ -1,232 +0,0 @@ -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/RepositoryOutieController.cs b/Flawless.Server/Controllers/RepositoryOutieController.cs new file mode 100644 index 0000000..eb425ac --- /dev/null +++ b/Flawless.Server/Controllers/RepositoryOutieController.cs @@ -0,0 +1,66 @@ +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; + +//todo Merging RepositoryManageController and RepositoryController +[ApiController, Authorize, Route("api")] +public class RepositoryOutieController(AppDbContext dbContext, UserManager userManager) : ControllerBase +{ + [HttpGet("repo_list")] + public async Task ListAllAvailableRepositoriesAsync([FromQuery] 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("repo_create")] + public async Task CreateRepositoryAsync([FromQuery] 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(); + } +} \ No newline at end of file diff --git a/Flawless.Server/Controllers/UserController.cs b/Flawless.Server/Controllers/UserController.cs index f920560..5dc08b2 100644 --- a/Flawless.Server/Controllers/UserController.cs +++ b/Flawless.Server/Controllers/UserController.cs @@ -15,7 +15,7 @@ public class UserController( ) : ControllerBase { - [HttpPost("update/info")] + [HttpPost("update_info")] public async Task UpdateUserInfoAsync(UserInfoModifyResponse r) { bool update = false; @@ -54,7 +54,7 @@ public class UserController( return Ok(); } - [HttpPost("update/email")] + [HttpPost("update_email")] public async Task UpdateEmailAsync(UserContactModifyResponse r) { if (string.IsNullOrWhiteSpace(r.Email)) @@ -68,7 +68,7 @@ public class UserController( } - [HttpPost("update/phone")] + [HttpPost("update_phone")] public async Task UpdatePhoneAsync(UserContactModifyResponse r) { if (string.IsNullOrWhiteSpace(r.Phone)) @@ -81,19 +81,11 @@ public class UserController( return Ok(); } - [HttpGet("get")] - public async Task> GetUserInfoAsync() - { - var self = (await userManager.GetUserAsync(HttpContext.User))!; - - // Return self as default - return Ok(GetUserInfoInternal(self, self)); - } - - [HttpGet("get/{username}")] - public async Task> GetUserInfoAsync(string username) + [HttpGet("get_info")] + public async Task> GetUserInfoAsync([FromQuery] string username) { var self = (await userManager.GetUserAsync(HttpContext.User))!; + if (string.IsNullOrWhiteSpace(username)) return Ok(GetUserInfoInternal(self, self)); var u = await userManager.FindByNameAsync(username); if (u == null) return BadRequest(new FailedResponse("User is not existed!")); @@ -101,8 +93,8 @@ public class UserController( return Ok(GetUserInfoInternal(u, self)); } - [HttpGet("query/{keyword}")] - public async Task>> GetUserInfoAsync(QueryPagesRequest r, string keyword) + [HttpGet("query_info")] + public async Task>> GetUserInfoAsync(QueryPagesRequest r, [FromQuery] string keyword) { var payload = await userManager.Users .Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword))) diff --git a/README.md b/README.md index 19d91fa..e549707 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # Flawless Version Control Yet another version control software for programmer, project manager, artist and designer which provides a **FLAWLESS** -felling on deploy and manage. \ No newline at end of file +felling on deploy and manage. + +# Create Interfaces + +``` +refitter http://localhost:5256/swagger/v1/swagger.json --namespace "Flawless.Client.Remote" --multiple-interfaces ByTag --multiple-files --cancellation-tokens --contracts-namespac "Flawless.Communication" --output ".\Flawless.Client\Service\Remote" +``` \ No newline at end of file