1
0

fix: Adjust api namespace

feat: Add Avalonia client base.
This commit is contained in:
Ca2didi 2025-03-27 02:06:48 +08:00
parent 437b5c6ab8
commit cdcd1d62ab
23 changed files with 1018 additions and 261 deletions

13
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"nswag.consolecore": {
"version": "14.2.0",
"commands": [
"nswag"
],
"rollForward": false
}
}
}

View File

@ -4,6 +4,8 @@
<option name="projectPerEditor">
<map>
<entry key="Flawless.Client.Avanonia/Views/MainWindow.axaml" value="Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj" />
<entry key="Flawless.Client/App.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />
</map>
</option>
</component>

View File

@ -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

View File

@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AActionMethodExecutor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdffdaf205cf54e098aa7d66ba76b38621de920_003F19_003Fe6aa02ee_003FActionMethodExecutor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApplication_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F22_003Fcb1aca4b_003FApplication_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentOutOfRangeException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F82_003F5d81019e_003FArgumentOutOfRangeException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssert_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea501b1a950043b99f3df638f1824d6143a18_003Fb8_003Fb16d6a68_003FAssert_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa5_003Ff3a8130e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

15
Flawless.Client/App.axaml Normal file
View File

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Flawless.Client.App"
xmlns:local="using:Flawless.Client"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -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<IFlawlessServer>(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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -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<HttpResponseMessage> 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<TokenInfo>(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<HttpResponseMessage> SendCommandAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Prefill this header
if (AuthenticationHeader != null)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationHeader);
return base.SendAsync(request, cancellationToken);
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<Folder Include="Service\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageReference Include="Refit" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flawless.Communication\Flawless.Communication.csproj" />
</ItemGroup>
</Project>

View File

@ -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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}

View File

@ -0,0 +1,458 @@
// <auto-generated>
// This code was generated by Refitter.
// </auto-generated>
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
{
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/delete/{username}")]
Task Delete(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/enable/{username}")]
Task Enable(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/disable/{username}")]
Task Disable(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/reset_password")]
Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/register")]
Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/login")]
Task<TokenInfo> Login([Body] LoginRequest body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/refresh")]
Task<TokenInfo> Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/logout_all")]
Task LogoutAll(CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/renew_password")]
Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/")]
Task Index(CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_repo")]
Task DeleteRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/get_info")]
Task GetInfo(string repositoryName, string userName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/archive_repo")]
Task ArchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")]
Task UnarchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/get_users")]
Task GetUsers(string repositoryName, string userName, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/update_user")]
Task UpdateUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_user")]
Task DeleteUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/fetch_manifest")]
Task FetchManifest(string userName, string repositoryName, [Query] string commitId, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/fetch_depot")]
Task FetchDepot(string userName, string repositoryName, [Query] string depotId, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/list_commit")]
Task ListCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/list_locked_files")]
Task ListLockedFiles(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo/{userName}/{repositoryName}/peek_commit")]
Task PeekCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/lock_file")]
Task LockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unlock_file")]
Task UnlockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Multipart]
[Post("/api/repo/{userName}/{repositoryName}/create_commit")]
Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<WorkspaceFile> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/repo_list")]
Task RepoList([Query, AliasAs("Offset")] int offset, [Query, AliasAs("Length")] int length, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo_create")]
Task RepoCreate([Query] string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_info")]
Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_email")]
Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_phone")]
Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/get_info")]
Task<UserInfoResponse> GetInfo([Query] string username, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/query_info")]
Task<UserInfoResponsePagedResponse> QueryInfo([Query] string keyword, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/user/delete")]
Task Delete(CancellationToken cancellationToken = default);
}
}
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
#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<UserInfoResponse> 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

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
namespace Flawless.Client.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
public string Greeting { get; } = "Welcome to Avalonia!";
}

View File

@ -0,0 +1,7 @@
using ReactiveUI;
namespace Flawless.Client.ViewModels;
public class ViewModelBase : ReactiveObject
{
}

View File

@ -0,0 +1,20 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Flawless.Client.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="Flawless.Client">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>

View File

@ -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);
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Flawless.Client.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -50,7 +50,7 @@ public class AuthenticationController(
}
[HttpPost("login")]
public async Task<ActionResult> LoginAsync(LoginRequest r)
public async Task<ActionResult<TokenInfo>> LoginAsync(LoginRequest r)
{
var user = await userManager.FindByNameAsync(r.Username);
if (user == null) return BadRequest(new FailedResponse("Invalid username or password."));

View File

@ -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<AppUser> userManager,
AppDbContext dbContext,
PathTransformer transformer,
@ -30,6 +30,178 @@ public class RepositoryControl(
}
#region Unresoted
[HttpPost("delete_repo")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<RepoUserRole>
{
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<IActionResult> 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<IActionResult> 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<IActionResult> DownloadManifestAsync(string userName, string repositoryName, string commitId)
[HttpGet("fetch_manifest")]
public async Task<IActionResult> 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<IActionResult> DownloadDepotAsync(string userName, string repositoryName, string depotId)
[HttpGet("fetch_depot")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;

View File

@ -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<AppUser> userManager) : ControllerBase
{
[HttpGet("list")]
public async Task<IActionResult> 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<RepositoryInfoResponse>
{
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<RepoUserRole>
{
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<IActionResult> 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<IActionResult> 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();
}
}

View File

@ -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<AppUser> userManager) : ControllerBase
{
[HttpGet("repo_list")]
public async Task<IActionResult> 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<RepositoryInfoResponse>
{
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<IActionResult> 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();
}
}

View File

@ -15,7 +15,7 @@ public class UserController(
) : ControllerBase
{
[HttpPost("update/info")]
[HttpPost("update_info")]
public async Task<IActionResult> UpdateUserInfoAsync(UserInfoModifyResponse r)
{
bool update = false;
@ -54,7 +54,7 @@ public class UserController(
return Ok();
}
[HttpPost("update/email")]
[HttpPost("update_email")]
public async Task<IActionResult> UpdateEmailAsync(UserContactModifyResponse r)
{
if (string.IsNullOrWhiteSpace(r.Email))
@ -68,7 +68,7 @@ public class UserController(
}
[HttpPost("update/phone")]
[HttpPost("update_phone")]
public async Task<IActionResult> UpdatePhoneAsync(UserContactModifyResponse r)
{
if (string.IsNullOrWhiteSpace(r.Phone))
@ -81,19 +81,11 @@ public class UserController(
return Ok();
}
[HttpGet("get")]
public async Task<ActionResult<UserInfoResponse>> GetUserInfoAsync()
{
var self = (await userManager.GetUserAsync(HttpContext.User))!;
// Return self as default
return Ok(GetUserInfoInternal(self, self));
}
[HttpGet("get/{username}")]
public async Task<ActionResult<UserInfoResponse>> GetUserInfoAsync(string username)
[HttpGet("get_info")]
public async Task<ActionResult<UserInfoResponse>> 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<ActionResult<PagedResponse<UserInfoResponse>>> GetUserInfoAsync(QueryPagesRequest r, string keyword)
[HttpGet("query_info")]
public async Task<ActionResult<PagedResponse<UserInfoResponse>>> GetUserInfoAsync(QueryPagesRequest r, [FromQuery] string keyword)
{
var payload = await userManager.Users
.Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword)))

View File

@ -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.
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"
```