1
This commit is contained in:
1 parent
3135189ea3
commit
a003d90b0e
21 files changed
+754
-145
No files matched your search
@@ -22,14 +22,7 @@ public sealed class LoginResult
|
||||
|
||||
public sealed record CurrentUser(string UserName, string? NickName);
|
||||
|
||||
public interface IAuthApi
|
||||
{
|
||||
Task<LoginResult> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default);
|
||||
Task LogoutAsync(CancellationToken cancellationToken = default);
|
||||
Task<CurrentUser> MeAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class AuthApi(ApiTransport transport, TokenSession session) : IAuthApi
|
||||
public sealed class AuthApi(ApiTransport transport, TokenSession session)
|
||||
{
|
||||
// 登录和退出串行执行,避免迟到的登录响应覆盖退出操作。
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
|
||||
@@ -9,8 +9,8 @@ public sealed class StoreApiClient : IDisposable
|
||||
{
|
||||
public TokenSession Session { get; } = new();
|
||||
public ApiTransport Transport { get; }
|
||||
public IAuthApi Auth { get; }
|
||||
public IUserApi Users { get; }
|
||||
public AuthApi Auth { get; }
|
||||
public UserApi Users { get; }
|
||||
|
||||
/// <param name="httpClient">可选的外部 HTTP 客户端,由调用方管理其生命周期。</param>
|
||||
public StoreApiClient(ApiClientOptions options, HttpClient? httpClient = null)
|
||||
|
||||
@@ -4,15 +4,8 @@ using Store.Management.Api.Transport;
|
||||
|
||||
namespace Store.Management.Api.Users;
|
||||
|
||||
public interface IUserApi
|
||||
{
|
||||
Task<PageResult<UserInfo>> SearchAsync(UserSearchRequest request, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> GetAsync(string? userName = null, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class UserApi(ApiTransport transport) : IUserApi
|
||||
public sealed class UserApi(ApiTransport transport)
|
||||
{
|
||||
public Task<PageResult<UserInfo>> SearchAsync(UserSearchRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using NLog;
|
||||
using Prism.Autofac;
|
||||
using Prism.Ioc;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Framework.Configs;
|
||||
using Store.Management.Framework.Extensions;
|
||||
using Store.Management.Framework.Services;
|
||||
@@ -131,7 +132,11 @@ public abstract class FrameworkApplication : PrismApplication
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
ShuttingDown = true;
|
||||
try { base.OnExit(e); }
|
||||
try
|
||||
{
|
||||
try { Container?.Resolve<StoreApiClient>().Dispose(); }
|
||||
finally { base.OnExit(e); }
|
||||
}
|
||||
finally
|
||||
{
|
||||
DispatcherUnhandledException -= OnDispatcherUnhandledException;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
xmlns:f="clr-namespace:Store.Management.Framework.Controls">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/MAX.Common.WPF;component/Resources/Generic.xaml" />
|
||||
|
||||
<ResourceDictionary Source="/Store.Management.Framework;component/Controls/DataGridEx/DataGridEx.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Api.Authentication;
|
||||
|
||||
namespace Store.Management.Framework.Services;
|
||||
|
||||
/// <summary>用户登录服务,复用应用 API 客户端的认证会话。</summary>
|
||||
public interface ILoginService
|
||||
{
|
||||
/// <summary>提交登录;passwordHash 为 UTF-8 密码的 32 位大写 MD5。</summary>
|
||||
Task LoginAsync(string userName, string passwordHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class LoginService(StoreApiClient apiClient) : ILoginService, ISingletonDependency
|
||||
{
|
||||
public Task LoginAsync(string userName, string passwordHash, CancellationToken cancellationToken = default)
|
||||
=> apiClient.Auth.LoginAsync(new LoginRequest
|
||||
{
|
||||
UserName = userName.Trim(),
|
||||
Password = passwordHash
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Autofac;
|
||||
using MAX.Common.WPF.Converters;
|
||||
using MAX.Common.WPF.Utilities;
|
||||
using MAX.Core;
|
||||
using MAX.Framework.Interfaces;
|
||||
using Store.Management.Api;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Store.Management.Framework
|
||||
{
|
||||
internal class Startup : IStartup
|
||||
{
|
||||
public int Priority => IntPriority.High;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
EnumDisplayConverter.EnumDisplayManager = new EnumDisplayManagerWPF(null);
|
||||
}
|
||||
|
||||
public void Register(ContainerBuilder builder)
|
||||
{
|
||||
var client = new StoreApiClient(new ApiClientOptions
|
||||
{
|
||||
BaseUrl = new Uri("http://localhost:14312/api/")
|
||||
});
|
||||
builder.Register(_ => client).SingleInstance().PreserveExistingDefaults();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HandyControl" Version="3.6.2.1" />
|
||||
<PackageReference Include="MAX.Common.WPF" Version="1.4.23" />
|
||||
<ProjectReference Include="..\Store.Management.Api\Store.Management.Api.csproj" />
|
||||
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
||||
<ProjectReference Include="..\Core\MAX.Framework\MAX.Framework.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,7 +2,6 @@ using MAX.Notification;
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Framework;
|
||||
using Store.Management.Notifications;
|
||||
using Store.Management.Services;
|
||||
@@ -12,20 +11,6 @@ using System.Windows;
|
||||
namespace Store.Management;
|
||||
public partial class App : FrameworkApplication
|
||||
{
|
||||
private readonly StoreApiClient? _apiClient;
|
||||
|
||||
public App() { }
|
||||
|
||||
public App(StoreApiClient apiClient)
|
||||
{
|
||||
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
}
|
||||
|
||||
private StoreApiClient CreateApiClient() => _apiClient ?? new(new ApiClientOptions
|
||||
{
|
||||
BaseUrl = new Uri("http://localhost:14312/api/")
|
||||
});
|
||||
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
if (Environment.GetCommandLineArgs().Contains("--grid-demo"))
|
||||
@@ -71,8 +56,6 @@ public partial class App : FrameworkApplication
|
||||
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
|
||||
containerRegistry.RegisterDialog<LoginWindow, LoginViewModel>();
|
||||
|
||||
//registry.RegisterInstance(CreateApiClient());
|
||||
// registry.Register<LoginViewModel>(provider => new LoginViewModel(provider.Resolve<StoreApiClient>().Auth));
|
||||
// registry.RegisterSingleton<SingletonProbe>();
|
||||
// registry.Register<TransientProbe>();
|
||||
// registry.RegisterSingleton<NotificationExampleState>();
|
||||
@@ -89,9 +72,4 @@ public partial class App : FrameworkApplication
|
||||
// registry.RegisterDialog<FrameworkTestDialog, FrameworkTestDialogViewModel>();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
//try { Container?.Resolve<StoreApiClient>().Dispose(); }
|
||||
//finally { base.OnExit(e); }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Prism.Commands;
|
||||
using Prism.Commands;
|
||||
using Prism.Services.Dialogs;
|
||||
using System.Windows.Input;
|
||||
using Store.Management.Api.Authentication;
|
||||
using Store.Management.Api.Transport;
|
||||
using Store.Management.Framework.ViewModels;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.Framework.Services;
|
||||
using Store.Management.Framework.ViewModels;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Store.Management.ViewModels;
|
||||
|
||||
/// <summary>登录校验、认证请求和窗口生命周期内的请求取消。</summary>
|
||||
public sealed class LoginViewModel() : ConfirmDialogViewModel, IDisposable
|
||||
public enum EEE
|
||||
{
|
||||
//private readonly IAuthApi _auth = auth ?? throw new ArgumentNullException(nameof(auth));
|
||||
[Description("管理员")]
|
||||
Admin,
|
||||
[Description("用户")]
|
||||
User
|
||||
}
|
||||
/// <summary>登录校验、认证请求和窗口生命周期内的请求取消。</summary>
|
||||
public sealed class LoginViewModel(ILoginService loginService) : ConfirmDialogViewModel, IDisposable
|
||||
{
|
||||
private EEE _ee;
|
||||
public EEE ee
|
||||
{
|
||||
get => _ee;
|
||||
set => SetProperty(ref _ee, value);
|
||||
}
|
||||
private readonly CancellationTokenSource _lifetime = new();
|
||||
private string _userName = "";
|
||||
private string _passwordHash = "";
|
||||
@@ -73,16 +87,12 @@ public sealed class LoginViewModel() : ConfirmDialogViewModel, IDisposable
|
||||
NotifyStateChanged();
|
||||
try
|
||||
{
|
||||
//await _auth.LoginAsync(new LoginRequest
|
||||
//{
|
||||
// UserName = UserName.Trim(),
|
||||
// Password = _passwordHash
|
||||
//}, _lifetime.Token);
|
||||
//if (!_disposed)
|
||||
//{
|
||||
// _authenticated = true;
|
||||
// OnRequestClose(new DialogResult(ButtonResult.OK));
|
||||
//}
|
||||
await loginService.LoginAsync(UserName, _passwordHash, _lifetime.Token);
|
||||
if (!_disposed)
|
||||
{
|
||||
_authenticated = true;
|
||||
OnRequestClose(new DialogResult(ButtonResult.OK));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_disposed)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Store.Management.ViewModels"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
Width="520"
|
||||
Height="620"
|
||||
MinWidth="440"
|
||||
@@ -42,32 +43,38 @@
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel>
|
||||
<Label
|
||||
Padding="0,0,0,8"
|
||||
Content="用户名"
|
||||
Target="{Binding ElementName=UserNameBox, Mode=OneWay}" />
|
||||
<TextBox
|
||||
x:Name="UserNameBox"
|
||||
Height="40"
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="用户名"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Label
|
||||
Padding="0,20,0,8"
|
||||
Content="密码"
|
||||
Target="{Binding ElementName=PasswordBox, Mode=OneWay}" />
|
||||
<PasswordBox
|
||||
x:Name="PasswordBox"
|
||||
Height="40"
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="密码"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
PasswordChanged="OnPasswordChanged" />
|
||||
<wpf:SpacingStackPanel Orientation="Vertical" VerticalSpacing="10">
|
||||
<ComboBox
|
||||
wpf:EnumBindingExtension.SelectedEnum="{Binding ee, Mode=TwoWay}"
|
||||
ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type vm:EEE},
|
||||
Converter={StaticResource EnumDisplayConverter}}"
|
||||
SelectedValuePath="Value" />
|
||||
<wpf:TitleContent
|
||||
Title="用户名"
|
||||
Placement="Left"
|
||||
TitleVerticalAlignment="Center"
|
||||
TitleWidth="50">
|
||||
<TextBox
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="用户名"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
|
||||
<wpf:TitleContent
|
||||
Title="密码"
|
||||
Placement="Left"
|
||||
TitleVerticalAlignment="Center"
|
||||
TitleWidth="50">
|
||||
<PasswordBox
|
||||
x:Name="PasswordBox"
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="密码"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
PasswordChanged="OnPasswordChanged" />
|
||||
</wpf:TitleContent>
|
||||
<TextBlock
|
||||
Margin="0,12,0,0"
|
||||
AutomationProperties.LiveSetting="Polite"
|
||||
@@ -90,7 +97,7 @@
|
||||
Content="{Binding LoginButtonText, Mode=OneWay}"
|
||||
IsDefault="True"
|
||||
Style="{StaticResource ButtonPrimary}" />
|
||||
</StackPanel>
|
||||
</wpf:SpacingStackPanel>
|
||||
</Border>
|
||||
<TextBlock
|
||||
Margin="0,20,0,0"
|
||||
|
||||
@@ -14,7 +14,6 @@ public partial class LoginWindow : UserControl
|
||||
{
|
||||
viewModel.ClearPasswordRequested += OnClearPasswordRequested;
|
||||
}
|
||||
UserNameBox.Focus();
|
||||
}
|
||||
|
||||
private void OnPasswordChanged(object sender, RoutedEventArgs e)
|
||||
|
||||
Reference in new issue
Block a user