diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
index 8859eb9..111cdb0 100644
--- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
+++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
@@ -5,7 +5,9 @@
diff --git a/.idea/.idea.Flawless-Version-Control/.idea/developer-tools.xml b/.idea/.idea.Flawless-Version-Control/.idea/developer-tools.xml
new file mode 100644
index 0000000..5421cc5
--- /dev/null
+++ b/.idea/.idea.Flawless-Version-Control/.idea/developer-tools.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user
index 739c965..90d91d6 100644
--- a/Flawless-Version-Control.sln.DotSettings.user
+++ b/Flawless-Version-Control.sln.DotSettings.user
@@ -16,6 +16,8 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/Flawless.Client/ApiHelper.cs b/Flawless.Client/ApiHelper.cs
new file mode 100644
index 0000000..765f383
--- /dev/null
+++ b/Flawless.Client/ApiHelper.cs
@@ -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($"Bearer {ApiHelper.Token}") : Task.FromResult(string.Empty)
+ };
+
+ var tempGateway = RestService.For(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 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;
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/App.axaml.cs b/Flawless.Client/App.axaml.cs
index 8889aed..89f3605 100644
--- a/Flawless.Client/App.axaml.cs
+++ b/Flawless.Client/App.axaml.cs
@@ -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(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()
};
}
diff --git a/Flawless.Client/AuthHeaderHandler.cs b/Flawless.Client/AuthHeaderHandler.cs
deleted file mode 100644
index b506429..0000000
--- a/Flawless.Client/AuthHeaderHandler.cs
+++ /dev/null
@@ -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 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
index 3609a1b..fdcdf74 100644
--- a/Flawless.Client/Flawless.Client.csproj
+++ b/Flawless.Client/Flawless.Client.csproj
@@ -27,6 +27,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Flawless.Client/Service/Remote_Generated.cs b/Flawless.Client/Service/Remote_Generated.cs
index 67d5579..62ba23b 100644
--- a/Flawless.Client/Service/Remote_Generated.cs
+++ b/Flawless.Client/Service/Remote_Generated.cs
@@ -36,6 +36,12 @@ namespace Flawless.Client.Remote
[Post("/api/admin/user/reset_password")]
Task ResetPassword([Body] ResetPasswordRequest 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/auth/status")]
+ Task Status(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")]
@@ -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))")]
diff --git a/Flawless.Client/ViewModels/LoginViewModel.cs b/Flawless.Client/ViewModels/LoginViewModel.cs
new file mode 100644
index 0000000..c064f5d
--- /dev/null
+++ b/Flawless.Client/ViewModels/LoginViewModel.cs
@@ -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 LoginCommand { get; }
+
+ public ReactiveCommand RegisterCommand { get; }
+
+ public ReactiveCommand 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!");
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/MainWindowViewModel.cs b/Flawless.Client/ViewModels/MainWindowViewModel.cs
index a8791f5..fc43a40 100644
--- a/Flawless.Client/ViewModels/MainWindowViewModel.cs
+++ b/Flawless.Client/ViewModels/MainWindowViewModel.cs
@@ -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);
+ }
}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/ServerSetupViewModel.cs b/Flawless.Client/ViewModels/ServerSetupViewModel.cs
new file mode 100644
index 0000000..00dafea
--- /dev/null
+++ b/Flawless.Client/ViewModels/ServerSetupViewModel.cs
@@ -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 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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/ViewModelBase.cs b/Flawless.Client/ViewModels/ViewModelBase.cs
index 1d53eb3..d95bd26 100644
--- a/Flawless.Client/ViewModels/ViewModelBase.cs
+++ b/Flawless.Client/ViewModels/ViewModelBase.cs
@@ -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
{
-}
\ No newline at end of file
+}
+
+public abstract partial class UserControlViewModelBase : ViewModelBase
+{
+ [Reactive(SetModifier = AccessModifier.Protected)]
+ private string _title;
+}
diff --git a/Flawless.Client/Views/LoginView.axaml b/Flawless.Client/Views/LoginView.axaml
new file mode 100644
index 0000000..e968f9d
--- /dev/null
+++ b/Flawless.Client/Views/LoginView.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flawless.Client/Views/LoginView.axaml.cs b/Flawless.Client/Views/LoginView.axaml.cs
new file mode 100644
index 0000000..0f07aba
--- /dev/null
+++ b/Flawless.Client/Views/LoginView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Flawless.Client.Views;
+
+public partial class LoginView : UserControl
+{
+ public LoginView()
+ {
+ InitializeComponent();
+ }
+
+}
\ No newline at end of file
diff --git a/Flawless.Client/Views/MainWindow.axaml b/Flawless.Client/Views/MainWindow.axaml
deleted file mode 100644
index af84898..0000000
--- a/Flawless.Client/Views/MainWindow.axaml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/Flawless.Client/Views/MainWindow.axaml.cs b/Flawless.Client/Views/MainWindow.axaml.cs
deleted file mode 100644
index 4ff525d..0000000
--- a/Flawless.Client/Views/MainWindow.axaml.cs
+++ /dev/null
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/Flawless.Client/Views/MainWindowView.axaml b/Flawless.Client/Views/MainWindowView.axaml
new file mode 100644
index 0000000..e5b1c9c
--- /dev/null
+++ b/Flawless.Client/Views/MainWindowView.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Flawless.Client/Views/MainWindowView.axaml.cs b/Flawless.Client/Views/MainWindowView.axaml.cs
new file mode 100644
index 0000000..55d6f69
--- /dev/null
+++ b/Flawless.Client/Views/MainWindowView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Flawless.Client.Views;
+
+public partial class MainWindowView : Window
+{
+ public MainWindowView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/Views/ServerSetupView.axaml b/Flawless.Client/Views/ServerSetupView.axaml
new file mode 100644
index 0000000..139d952
--- /dev/null
+++ b/Flawless.Client/Views/ServerSetupView.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flawless.Client/Views/ServerSetupView.axaml.cs b/Flawless.Client/Views/ServerSetupView.axaml.cs
new file mode 100644
index 0000000..3135168
--- /dev/null
+++ b/Flawless.Client/Views/ServerSetupView.axaml.cs
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Communication/Response/ServerStatusResponse.cs b/Flawless.Communication/Response/ServerStatusResponse.cs
new file mode 100644
index 0000000..b0991c8
--- /dev/null
+++ b/Flawless.Communication/Response/ServerStatusResponse.cs
@@ -0,0 +1,6 @@
+namespace Flawless.Communication.Response;
+
+public record ServerStatusResponse
+{
+ public required bool AllowPublicRegister { get; set; }
+}
\ No newline at end of file
diff --git a/Flawless.Communication/Shared/TokenInfo.cs b/Flawless.Communication/Shared/TokenInfo.cs
index 9445db1..04f41ea 100644
--- a/Flawless.Communication/Shared/TokenInfo.cs
+++ b/Flawless.Communication/Shared/TokenInfo.cs
@@ -3,4 +3,6 @@
public record TokenInfo
{
public required string Token { get; set; }
+
+ public DateTime? Expiration { get; set; }
}
\ No newline at end of file
diff --git a/Flawless.Server/Controllers/AuthenticationController.cs b/Flawless.Server/Controllers/AuthenticationController.cs
index 1312e20..69558a3 100644
--- a/Flawless.Server/Controllers/AuthenticationController.cs
+++ b/Flawless.Server/Controllers/AuthenticationController.cs
@@ -21,6 +21,15 @@ public class AuthenticationController(
ILogger logger)
: ControllerBase
{
+
+ [HttpGet("status")]
+ public Task> GetServerStatusAsync()
+ {
+ return Task.FromResult>(Ok(new ServerStatusResponse()
+ {
+ AllowPublicRegister = true
+ }));
+ }
[HttpPost("register")]
public async Task 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
{
diff --git a/README.md b/README.md
index e549707..8036cb4 100644
--- a/README.md
+++ b/README.md
@@ -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"
```
\ No newline at end of file