feat: Add server choice and login gui.
This commit is contained in:
parent
cdcd1d62ab
commit
ec75cf88f3
@ -5,7 +5,9 @@
|
||||
<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/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/MainWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
6
.idea/.idea.Flawless-Version-Control/.idea/developer-tools.xml
generated
Normal file
6
.idea/.idea.Flawless-Version-Control/.idea/developer-tools.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeveloperToolsToolWindowSettingsV1" lastSelectedContentNodeId="base64-encoder-decoder">
|
||||
<developerToolsConfigurations />
|
||||
</component>
|
||||
</project>
|
||||
@ -16,6 +16,8 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd991417b721d4ddab50a2b715d0ad696b138_003Fa2_003Fdb3874bc_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5c_003F228dd86b_003FReactiveObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARefitSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa2b3a0b86cd053ed984c171eb448b551b9a2a0c89c5a68191996cffac5d85d2b_003FRefitSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fce37be1a06b16c6faa02038d2cc477dd3bca5b217ceeb41c5f2ad45c1bf9_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASignInManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F40411c547364428dafc988a7615774e28b910_003F6d_003F1a409232_003FSignInManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
63
Flawless.Client/ApiHelper.cs
Normal file
63
Flawless.Client/ApiHelper.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Client.Remote;
|
||||
using Refit;
|
||||
|
||||
namespace Flawless.Client;
|
||||
|
||||
public static class ApiHelper
|
||||
{
|
||||
private static IFlawlessServer? _gateway;
|
||||
|
||||
public static void ClearGateway()
|
||||
{
|
||||
Status = null;
|
||||
ServerUrl = null;
|
||||
_gateway = null;
|
||||
Token = null;
|
||||
}
|
||||
|
||||
public static async Task SetGatewayAsync(string host)
|
||||
{
|
||||
var setting = new RefitSettings
|
||||
{
|
||||
AuthorizationHeaderValueGetter = (req, ct) => ApiHelper.Token != null ?
|
||||
Task.FromResult<string>($"Bearer {ApiHelper.Token}") : Task.FromResult(string.Empty)
|
||||
};
|
||||
|
||||
var tempGateway = RestService.For<IFlawlessServer>(host, setting);
|
||||
Status = await tempGateway.Status();
|
||||
ServerUrl = host;
|
||||
_gateway = tempGateway;
|
||||
}
|
||||
|
||||
public static string? ServerUrl { get; private set; }
|
||||
|
||||
public static ServerStatusResponse? Status { get; private set; }
|
||||
|
||||
public static TokenInfo? Token { get; set; }
|
||||
|
||||
public static bool IsGatewayReady => _gateway != null;
|
||||
public static IFlawlessServer Gateway => _gateway ?? throw new InvalidProgramException("Not set gateway yet!");
|
||||
|
||||
|
||||
public static bool RequireRefreshToken()
|
||||
{
|
||||
if (Token == null) return true;
|
||||
if (DateTime.UtcNow.AddMinutes(1) > Token.Expiration) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> TryRefreshTokenAsync()
|
||||
{
|
||||
if (Token == null) return false;
|
||||
try { Token = await Gateway.Refresh(Token); }
|
||||
catch (Exception e)
|
||||
{
|
||||
await Console.Error.WriteLineAsync(e.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,25 @@
|
||||
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
|
||||
desktop.MainWindow = new MainWindowView
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
DataContext = new MainWindowViewModel()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,10 @@
|
||||
<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="ReactiveUI.SourceGenerators" Version="2.1.27">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Refit" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -36,6 +36,12 @@ namespace Flawless.Client.Remote
|
||||
[Post("/api/admin/user/reset_password")]
|
||||
Task ResetPassword([Body] ResetPasswordRequest 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/auth/status")]
|
||||
Task<ServerStatusResponse> Status(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")]
|
||||
@ -304,6 +310,15 @@ namespace Flawless.Client.Remote
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class ServerStatusResponse
|
||||
{
|
||||
|
||||
[JsonPropertyName("allowPublicRegister")]
|
||||
public bool AllowPublicRegister { 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
|
||||
{
|
||||
@ -312,6 +327,9 @@ namespace Flawless.Client.Remote
|
||||
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
|
||||
public string Token { get; set; }
|
||||
|
||||
[JsonPropertyName("expiration")]
|
||||
public System.DateTimeOffset? Expiration { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
|
||||
76
Flawless.Client/ViewModels/LoginViewModel.cs
Normal file
76
Flawless.Client/ViewModels/LoginViewModel.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Client.Remote;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using Refit;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public partial class LoginViewModel : UserControlViewModelBase
|
||||
{
|
||||
[Reactive] private string _username = String.Empty;
|
||||
|
||||
[Reactive] private string _password = String.Empty;
|
||||
|
||||
[Reactive] private string _issue = String.Empty;
|
||||
|
||||
private MainWindowViewModel _main;
|
||||
|
||||
public ReactiveCommand<Unit, Unit> LoginCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> RegisterCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> ChooseServerCommand { get; }
|
||||
|
||||
public LoginViewModel(MainWindowViewModel main)
|
||||
{
|
||||
_main = main;
|
||||
Title = $"Login into '{ApiHelper.ServerUrl}'";
|
||||
|
||||
var canLogin = this.WhenAnyValue(
|
||||
x => x.Username,
|
||||
x => x.Password,
|
||||
(user, pass) => !string.IsNullOrEmpty(user) && user.Length > 3 && !string.IsNullOrEmpty(pass) && pass.Length >= 6
|
||||
);
|
||||
|
||||
LoginCommand = ReactiveCommand.CreateFromTask(OnLoginAsync, canLogin);
|
||||
RegisterCommand = ReactiveCommand.Create(OnRegister);
|
||||
ChooseServerCommand = ReactiveCommand.Create(OnChooseServer);
|
||||
}
|
||||
|
||||
private void OnChooseServer()
|
||||
{
|
||||
_main.RedirectToServerSetup();
|
||||
}
|
||||
|
||||
private void OnRegister()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private async Task OnLoginAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
ApiHelper.Token = await ApiHelper.Gateway.Login(new LoginRequest
|
||||
{
|
||||
Username = _username,
|
||||
Password = _password
|
||||
});
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex.Content}");
|
||||
Issue = ex.Content ?? String.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex}");
|
||||
Issue = ex.Message;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Login as '{Username}' success!");
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,25 @@
|
||||
namespace Flawless.Client.ViewModels;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
public string Greeting { get; } = "Welcome to Avalonia!";
|
||||
[Reactive(SetModifier = AccessModifier.Protected)]
|
||||
private UserControlViewModelBase _currentView;
|
||||
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
CurrentView = new ServerSetupViewModel(this);
|
||||
}
|
||||
|
||||
public void RedirectToLogin()
|
||||
{
|
||||
CurrentView = new LoginViewModel(this);
|
||||
}
|
||||
|
||||
public void RedirectToServerSetup()
|
||||
{
|
||||
CurrentView = new ServerSetupViewModel(this);
|
||||
}
|
||||
}
|
||||
59
Flawless.Client/ViewModels/ServerSetupViewModel.cs
Normal file
59
Flawless.Client/ViewModels/ServerSetupViewModel.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using Refit;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public partial class ServerSetupViewModel : UserControlViewModelBase
|
||||
{
|
||||
[Reactive] private string _host = "http://localhost:5256/";
|
||||
|
||||
[Reactive] private string? _issue;
|
||||
|
||||
private MainWindowViewModel _main;
|
||||
|
||||
public ReactiveCommand<Unit, Unit> SetHostCommand { get; }
|
||||
|
||||
|
||||
private static readonly Regex httpRegex = new Regex(
|
||||
@"^(?i)https?://(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(/[\w\-\.~%!$&'()*+,;=:@/?#]*)?(?-i)$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public ServerSetupViewModel(MainWindowViewModel main)
|
||||
{
|
||||
_main = main;
|
||||
Title = "Connect to a Flawless Server";
|
||||
|
||||
// Must clear this gateway
|
||||
if (ApiHelper.IsGatewayReady) ApiHelper.ClearGateway();
|
||||
|
||||
SetHostCommand = ReactiveCommand.CreateFromTask(
|
||||
FetchServerDataAsync,
|
||||
this.WhenAnyValue(x => x.Host, s => !string.IsNullOrWhiteSpace(s)));
|
||||
}
|
||||
|
||||
private async Task FetchServerDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Issue = string.Empty;
|
||||
await ApiHelper.SetGatewayAsync(Host);
|
||||
_main.RedirectToLogin();
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
||||
Issue = ex.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
||||
Issue = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +1,15 @@
|
||||
using ReactiveUI;
|
||||
using Avalonia.Controls;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public class ViewModelBase : ReactiveObject
|
||||
public abstract class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract partial class UserControlViewModelBase : ViewModelBase
|
||||
{
|
||||
[Reactive(SetModifier = AccessModifier.Protected)]
|
||||
private string _title;
|
||||
}
|
||||
|
||||
27
Flawless.Client/Views/LoginView.axaml
Normal file
27
Flawless.Client/Views/LoginView.axaml
Normal file
@ -0,0 +1,27 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400"
|
||||
x:Class="Flawless.Client.Views.LoginView"
|
||||
x:DataType="vm:LoginViewModel">
|
||||
|
||||
<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:LoginViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="10">
|
||||
<Image Source="/Assets/avalonia-logo.ico" Margin="20 20" MaxWidth="150"/>
|
||||
<TextBox Watermark="Username" Text="{Binding Username, Mode=TwoWay}"/>
|
||||
<TextBox Watermark="Password" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button Content="Login" Command="{Binding LoginCommand}"/>
|
||||
<Button Content="Register" Command="{Binding RegisterCommand}"/>
|
||||
<Button Content="Choose Server..." Command="{Binding ChooseServerCommand}"/>
|
||||
</StackPanel>
|
||||
<Label Content="{Binding Issue}" Foreground="Red"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
12
Flawless.Client/Views/LoginView.axaml.cs
Normal file
12
Flawless.Client/Views/LoginView.axaml.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Flawless.Client.Views;
|
||||
|
||||
public partial class LoginView : UserControl
|
||||
{
|
||||
public LoginView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
23
Flawless.Client/Views/MainWindowView.axaml
Normal file
23
Flawless.Client/Views/MainWindowView.axaml
Normal file
@ -0,0 +1,23 @@
|
||||
<Window x:Class="Flawless.Client.Views.MainWindowView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||
Title="{Binding CurrentView.Title}"
|
||||
d:DesignHeight="400" d:DesignWidth="400"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
CanResize="False"
|
||||
Width="400" Height="400"
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel Margin="30">
|
||||
<TransitioningContentControl Content="{Binding CurrentView}" />
|
||||
</StackPanel>
|
||||
</Window>
|
||||
11
Flawless.Client/Views/MainWindowView.axaml.cs
Normal file
11
Flawless.Client/Views/MainWindowView.axaml.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Flawless.Client.Views;
|
||||
|
||||
public partial class MainWindowView : Window
|
||||
{
|
||||
public MainWindowView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
22
Flawless.Client/Views/ServerSetupView.axaml
Normal file
22
Flawless.Client/Views/ServerSetupView.axaml
Normal file
@ -0,0 +1,22 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Flawless.Client.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400"
|
||||
x:Class="Flawless.Client.Views.ServerSetupView"
|
||||
x:DataType="vm:ServerSetupViewModel">
|
||||
|
||||
<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:ServerSetupViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel Spacing="10" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||
<Image Source="/Assets/avalonia-logo.ico" Margin="20 20" MaxWidth="150"/>
|
||||
<TextBox Watermark="Host" Text="{Binding Host, Mode=TwoWay}"/>
|
||||
<Button Content="Connect" Command="{Binding SetHostCommand}"/>
|
||||
<Label Content="{Binding Issue}" Foreground="Red"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
13
Flawless.Client/Views/ServerSetupView.axaml.cs
Normal file
13
Flawless.Client/Views/ServerSetupView.axaml.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Flawless.Client.Views;
|
||||
|
||||
public partial class ServerSetupView : UserControl
|
||||
{
|
||||
public ServerSetupView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
6
Flawless.Communication/Response/ServerStatusResponse.cs
Normal file
6
Flawless.Communication/Response/ServerStatusResponse.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record ServerStatusResponse
|
||||
{
|
||||
public required bool AllowPublicRegister { get; set; }
|
||||
}
|
||||
@ -3,4 +3,6 @@
|
||||
public record TokenInfo
|
||||
{
|
||||
public required string Token { get; set; }
|
||||
|
||||
public DateTime? Expiration { get; set; }
|
||||
}
|
||||
@ -21,6 +21,15 @@ public class AuthenticationController(
|
||||
ILogger<AuthenticationController> logger)
|
||||
: ControllerBase
|
||||
{
|
||||
|
||||
[HttpGet("status")]
|
||||
public Task<ActionResult<ServerStatusResponse>> GetServerStatusAsync()
|
||||
{
|
||||
return Task.FromResult<ActionResult<ServerStatusResponse>>(Ok(new ServerStatusResponse()
|
||||
{
|
||||
AllowPublicRegister = true
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult> PublicRegisterAsync(RegisterRequest request)
|
||||
@ -61,18 +70,19 @@ public class AuthenticationController(
|
||||
var refreshToken = tokenService.GenerateRefreshToken();
|
||||
var claims = await GetClaimsAsync(user, refreshToken);
|
||||
var jwtToken = tokenService.GenerateToken(claims);
|
||||
var exp = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime);
|
||||
var refKey = new AppUserRefreshKey
|
||||
{
|
||||
UserId = user.Id,
|
||||
RefreshToken = refreshToken,
|
||||
ExpireIn = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime),
|
||||
ExpireIn = exp,
|
||||
};
|
||||
|
||||
dbContext.RefreshTokens.Add(refKey);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await userManager.AddLoginAsync(user, new UserLoginInfo("login-interface", "", null));
|
||||
|
||||
return Ok(new TokenInfo { Token = jwtToken });
|
||||
return Ok(new TokenInfo { Token = jwtToken, Expiration = exp });
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
@ -106,6 +116,7 @@ public class AuthenticationController(
|
||||
refreshToken = tokenService.GenerateRefreshToken();
|
||||
var claims = await GetClaimsAsync(user, refreshToken);
|
||||
var newJwtToken = tokenService.GenerateToken(claims);
|
||||
var exp = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime);
|
||||
|
||||
// Reassign a new key.
|
||||
set.Remove(tk);
|
||||
@ -113,10 +124,10 @@ public class AuthenticationController(
|
||||
{
|
||||
UserId = user.Id,
|
||||
RefreshToken = refreshToken,
|
||||
ExpireIn = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime),
|
||||
ExpireIn = exp,
|
||||
});
|
||||
|
||||
return Ok(new TokenInfo { Token = newJwtToken });
|
||||
return Ok(new TokenInfo { Token = newJwtToken, Expiration = exp });
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -6,5 +6,5 @@ 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"
|
||||
refitter http://localhost:5256/swagger/v1/swagger.json --namespace "Flawless.Client.Remote" --cancellation-tokens --contracts-namespac "Flawless.Communication" --output ".\Flawless.Client\Service\Remote_Generated.cs"
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user