XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

HaloPixelToolBox

【WinUI3】基于USB HID通讯的花再音响工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/HaloPixelToolBox

重构管理员登录与会话管理,支持自动恢复

重构管理端管理员登录与会话管理机制,新增 AdministratorSessionManager 实现自动恢复/重连、凭据本地加密保存(DPAPI)、统一登录状态管理及事件通知。引入 WindowsDataProtection 工具类,CacheProfile 支持加密密码字段。登录/登出逻辑迁移至 SettingPage,主页仅作功能入口。管理页面统一通过会话管理器校验登录状态,提升安全性与体验。优化设置页 UI,支持凭据保存与自动登录,精简主页。调整默认服务器地址,完善事件注册、UI 同步及代码风格。

40064a8
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

13 个文件 +602 -182
Modified Backend/HaloPixelToolBox.Backend/Profiles/CacheProfiles/CacheProfile.cs +62 -1
@@ -1,10 +1,15 @@
1 using XFEExtension.NetCore.AutoConfig;
1 using System.Security.Cryptography;
2 using System.Text;
3 using HaloPixelToolBox.Backend.Utilities;
4 using XFEExtension.NetCore.AutoConfig;
2 5 using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
3 6
4 7 namespace HaloPixelToolBox.Backend.Profiles.CacheProfiles;
5 8
6 9 public partial class CacheProfile : XFEProfile
7 10 {
11 private static readonly byte[] s_passwordEntropy = Encoding.UTF8.GetBytes("HaloPixelToolBox.Backend.AdministratorCredential.v1");
12
8 13 public CacheProfile() => ProfilePath = $@"{AppPathHelper.CacheProfile}\{nameof(CacheProfile)}";
9 14
10 15 [ProfileProperty]
@@ -12,4 +17,60 @@ public partial class CacheProfile : XFEProfile
12 17
13 18 [ProfileProperty]
14 19 private string _account = "admin";
20
21 /// <summary>
22 /// 由 Windows 当前用户数据保护机制加密后的管理员密码。
23 /// </summary>
24 [ProfileProperty]
25 private string _encryptedPassword = string.Empty;
26
27 /// <summary>
28 /// 用于管理端自动登录的密码;磁盘中仅保存当前 Windows 用户可解密的密文。
29 /// </summary>
30 public static string Password
31 {
32 get
33 {
34 if (string.IsNullOrWhiteSpace(EncryptedPassword))
35 return string.Empty;
36
37 try
38 {
39 var encryptedBytes = Convert.FromBase64String(EncryptedPassword);
40 var passwordBytes = WindowsDataProtection.Unprotect(encryptedBytes, s_passwordEntropy);
41 try
42 {
43 return Encoding.UTF8.GetString(passwordBytes);
44 }
45 finally
46 {
47 CryptographicOperations.ZeroMemory(passwordBytes);
48 }
49 }
50 catch (Exception ex) when (ex is FormatException or System.ComponentModel.Win32Exception)
51 {
52 EncryptedPassword = string.Empty;
53 return string.Empty;
54 }
55 }
56 set
57 {
58 if (string.IsNullOrEmpty(value))
59 {
60 EncryptedPassword = string.Empty;
61 return;
62 }
63
64 var passwordBytes = Encoding.UTF8.GetBytes(value);
65 try
66 {
67 var encryptedBytes = WindowsDataProtection.Protect(passwordBytes, s_passwordEntropy);
68 EncryptedPassword = Convert.ToBase64String(encryptedBytes);
69 }
70 finally
71 {
72 CryptographicOperations.ZeroMemory(passwordBytes);
73 }
74 }
75 }
15 76 }
Added Backend/HaloPixelToolBox.Backend/Utilities/AdministratorSessionManager.cs +219 -0
@@ -0,0 +1,219 @@
1 using HaloPixelToolBox.Backend.Profiles.CacheProfiles;
2 using HaloPixelToolBox.Backend.Profiles.CrossVersionProfiles;
3 using HaloPixelToolBox.Core.Models.User;
4 using HaloPixelToolBox.Core.Utilities;
5 using System.Net;
6 using XFEExtension.NetCore.ServerInteractive.Models.RequesterModels;
7
8 namespace HaloPixelToolBox.Backend.Utilities;
9
10 /// <summary>
11 /// 统一维护管理端登录状态,并负责启动时恢复或重新创建管理员会话。
12 /// </summary>
13 public static class AdministratorSessionManager
14 {
15 private static readonly SemaphoreSlim s_authenticationLock = new(1, 1);
16
17 public static event EventHandler? StateChanged;
18
19 public static MyUserFaceInfo? CurrentUser { get; private set; }
20
21 public static bool IsLoggedIn { get; private set; }
22
23 public static bool IsBusy { get; private set; }
24
25 public static string StatusText { get; private set; } = "正在准备自动登录...";
26
27 public static string DisplayName => CurrentUser?.NickName ?? (IsLoggedIn ? CacheProfile.Account : "未登录");
28
29 /// <summary>
30 /// 优先恢复现有会话;会话失效时,使用本机保存的账号密码自动重新登录。
31 /// </summary>
32 public static async Task<bool> InitializeAsync(bool forceReconnect = false)
33 {
34 await s_authenticationLock.WaitAsync();
35 try
36 {
37 if (IsLoggedIn && !forceReconnect)
38 return true;
39
40 SetBusy(true, forceReconnect ? "正在重新连接服务器..." : "正在自动登录...");
41 if (!await ConnectAsync(forceReconnect))
42 {
43 SetLoggedOut("服务器连接失败,请在设置中检查服务器地址");
44 return false;
45 }
46
47 if (!string.IsNullOrWhiteSpace(CacheProfile.Session))
48 {
49 DataManager.ClientRequester.Session = CacheProfile.Session;
50 var restoredUser = await RestoreSessionCoreAsync();
51 if (restoredUser is not null)
52 {
53 CompleteLogin(restoredUser, CacheProfile.Session, "已自动恢复登录");
54 return true;
55 }
56
57 ClearSession();
58 StatusText = "登录会话已过期,正在使用保存的凭据重新登录...";
59 NotifyStateChanged();
60 }
61
62 if (string.IsNullOrWhiteSpace(CacheProfile.Account) || string.IsNullOrWhiteSpace(CacheProfile.Password))
63 {
64 SetLoggedOut("请输入管理员账号和密码,成功后将自动登录");
65 return false;
66 }
67
68 return await LoginCoreAsync(CacheProfile.Account, CacheProfile.Password, "已自动登录");
69 }
70 catch (Exception ex)
71 {
72 SetLoggedOut($"自动登录失败:{ex.Message}");
73 return false;
74 }
75 finally
76 {
77 SetBusy(false);
78 s_authenticationLock.Release();
79 }
80 }
81
82 /// <summary>
83 /// 使用用户输入的管理员凭据登录,并在成功后保存用于下次自动登录。
84 /// </summary>
85 public static async Task<bool> LoginAsync(string account, string password)
86 {
87 account = account.Trim();
88 if (string.IsNullOrWhiteSpace(account) || string.IsNullOrWhiteSpace(password))
89 {
90 SetLoggedOut("请输入管理员账号和密码");
91 return false;
92 }
93
94 await s_authenticationLock.WaitAsync();
95 try
96 {
97 SetBusy(true, "正在登录管理员账号...");
98 if (!await ConnectAsync(true))
99 {
100 SetLoggedOut("服务器连接失败,请在设置中检查服务器地址");
101 return false;
102 }
103
104 return await LoginCoreAsync(account, password, "已登录");
105 }
106 catch (Exception ex)
107 {
108 SetLoggedOut($"登录失败:{ex.Message}");
109 return false;
110 }
111 finally
112 {
113 SetBusy(false);
114 s_authenticationLock.Release();
115 }
116 }
117
118 /// <summary>
119 /// 退出管理端,并清除本机保存的密码,防止随后再次自动登录。
120 /// </summary>
121 public static async Task LogoutAsync()
122 {
123 await s_authenticationLock.WaitAsync();
124 try
125 {
126 ClearSession();
127 CacheProfile.Password = string.Empty;
128 SetLoggedOut("已退出登录;下次需要重新输入密码");
129 }
130 finally
131 {
132 s_authenticationLock.Release();
133 }
134 }
135
136 private static async Task<bool> ConnectAsync(bool forceReconnect)
137 {
138 var serverAddress = SystemProfile.ServerAddress.Trim();
139 if (!Uri.TryCreate(serverAddress, UriKind.Absolute, out var uri) ||
140 (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
141 {
142 StatusText = "服务器地址无效,请输入完整的 HTTP 或 HTTPS 地址";
143 NotifyStateChanged();
144 return false;
145 }
146
147 return await DataManager.InitializeAsync([serverAddress], forceReconnect);
148 }
149
150 private static async Task<MyUserFaceInfo?> RestoreSessionCoreAsync()
151 {
152 var response = await DataManager.ClientRequester.Request<MyUserFaceInfo>("relogin");
153 return response.StatusCode == HttpStatusCode.OK && IsAdministrator(response.Result)
154 ? response.Result
155 : null;
156 }
157
158 private static async Task<bool> LoginCoreAsync(string account, string password, string successMessage)
159 {
160 ClearSession();
161 var response = await DataManager.ClientRequester.Request<UserLoginResult<MyUserFaceInfo>>("login", account, password);
162 if (response.StatusCode != HttpStatusCode.OK || response.Result?.UserInfo is null)
163 {
164 SetLoggedOut("登录失败,请检查账号或密码");
165 return false;
166 }
167
168 if (!IsAdministrator(response.Result.UserInfo))
169 {
170 ClearSession();
171 SetLoggedOut("该账号没有管理员权限");
172 return false;
173 }
174
175 CacheProfile.Account = account;
176 CacheProfile.Password = password;
177 CompleteLogin(response.Result.UserInfo, response.Result.Session, successMessage);
178 return true;
179 }
180
181 private static bool IsAdministrator(MyUserFaceInfo? user) =>
182 user is not null && user.PermissionLevel >= (int)UserRole.管理员;
183
184 private static void CompleteLogin(MyUserFaceInfo user, string session, string successMessage)
185 {
186 DataManager.ClientRequester.Session = session;
187 CacheProfile.Session = session;
188 CurrentUser = user;
189 IsLoggedIn = true;
190 StatusText = $"{successMessage}:{user.NickName}({(UserRole)user.PermissionLevel})";
191 NotifyStateChanged();
192 }
193
194 private static void ClearSession()
195 {
196 DataManager.ClientRequester.Session = string.Empty;
197 CacheProfile.Session = string.Empty;
198 CurrentUser = null;
199 IsLoggedIn = false;
200 }
201
202 private static void SetLoggedOut(string statusText)
203 {
204 CurrentUser = null;
205 IsLoggedIn = false;
206 StatusText = statusText;
207 NotifyStateChanged();
208 }
209
210 private static void SetBusy(bool isBusy, string? statusText = null)
211 {
212 IsBusy = isBusy;
213 if (statusText is not null)
214 StatusText = statusText;
215 NotifyStateChanged();
216 }
217
218 private static void NotifyStateChanged() => StateChanged?.Invoke(null, EventArgs.Empty);
219 }
Added Backend/HaloPixelToolBox.Backend/Utilities/WindowsDataProtection.cs +95 -0
@@ -0,0 +1,95 @@
1 using System.ComponentModel;
2 using System.Runtime.InteropServices;
3
4 namespace HaloPixelToolBox.Backend.Utilities;
5
6 /// <summary>
7 /// 使用 Windows DPAPI 加密仅允许当前 Windows 用户解密的本地数据。
8 /// </summary>
9 internal static partial class WindowsDataProtection
10 {
11 private const int CryptProtectUiForbidden = 0x1;
12
13 public static byte[] Protect(byte[] data, byte[] entropy) => Transform(data, entropy, protect: true);
14
15 public static byte[] Unprotect(byte[] data, byte[] entropy) => Transform(data, entropy, protect: false);
16
17 private static byte[] Transform(byte[] data, byte[] entropy, bool protect)
18 {
19 var inputBlob = CreateBlob(data);
20 var entropyBlob = CreateBlob(entropy);
21 DataBlob outputBlob = default;
22 try
23 {
24 var succeeded = protect
25 ? CryptProtectData(ref inputBlob, null, ref entropyBlob, IntPtr.Zero, IntPtr.Zero, CryptProtectUiForbidden, out outputBlob)
26 : CryptUnprotectData(ref inputBlob, IntPtr.Zero, ref entropyBlob, IntPtr.Zero, IntPtr.Zero, CryptProtectUiForbidden, out outputBlob);
27
28 if (!succeeded)
29 throw new Win32Exception(Marshal.GetLastWin32Error());
30
31 var output = new byte[outputBlob.Size];
32 Marshal.Copy(outputBlob.Data, output, 0, output.Length);
33 return output;
34 }
35 finally
36 {
37 FreeInputBlob(ref inputBlob);
38 FreeInputBlob(ref entropyBlob);
39 if (outputBlob.Data != IntPtr.Zero)
40 LocalFree(outputBlob.Data);
41 }
42 }
43
44 private static DataBlob CreateBlob(byte[] data)
45 {
46 var blob = new DataBlob { Size = data.Length };
47 if (data.Length == 0)
48 return blob;
49
50 blob.Data = Marshal.AllocHGlobal(data.Length);
51 Marshal.Copy(data, 0, blob.Data, data.Length);
52 return blob;
53 }
54
55 private static void FreeInputBlob(ref DataBlob blob)
56 {
57 if (blob.Data == IntPtr.Zero)
58 return;
59
60 Marshal.FreeHGlobal(blob.Data);
61 blob = default;
62 }
63
64 [StructLayout(LayoutKind.Sequential)]
65 private struct DataBlob
66 {
67 public int Size;
68 public IntPtr Data;
69 }
70
71 [LibraryImport("Crypt32.dll", EntryPoint = "CryptProtectData", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
72 [return: MarshalAs(UnmanagedType.Bool)]
73 private static partial bool CryptProtectData(
74 ref DataBlob dataIn,
75 string? description,
76 ref DataBlob optionalEntropy,
77 IntPtr reserved,
78 IntPtr promptStruct,
79 int flags,
80 out DataBlob dataOut);
81
82 [LibraryImport("Crypt32.dll", EntryPoint = "CryptUnprotectData", SetLastError = true)]
83 [return: MarshalAs(UnmanagedType.Bool)]
84 private static partial bool CryptUnprotectData(
85 ref DataBlob dataIn,
86 IntPtr description,
87 ref DataBlob optionalEntropy,
88 IntPtr reserved,
89 IntPtr promptStruct,
90 int flags,
91 out DataBlob dataOut);
92
93 [LibraryImport("Kernel32.dll", EntryPoint = "LocalFree")]
94 private static partial IntPtr LocalFree(IntPtr memory);
95 }
Modified Backend/HaloPixelToolBox.Backend/ViewModels/AddressResolveManagePageViewModel.cs +10 -7
@@ -1,5 +1,6 @@
1 1 using CommunityToolkit.Mvvm.ComponentModel;
2 2 using CommunityToolkit.Mvvm.Input;
3 using HaloPixelToolBox.Backend.Utilities;
3 4 using HaloPixelToolBox.Core.Models.Bar;
4 5 using HaloPixelToolBox.Core.Utilities;
5 6 using System.Collections.ObjectModel;
@@ -16,7 +17,7 @@ public partial class AddressResolveManagePageViewModel : ViewModelBase
16 17 [ObservableProperty] public partial string ModuleName { get; set; } = "cloudmusic.dll";
17 18 [ObservableProperty] public partial string BaseAddressText { get; set; } = string.Empty;
18 19 [ObservableProperty] public partial string OffsetsText { get; set; } = string.Empty;
19 [ObservableProperty] public partial string StatusText { get; set; } = "请先在主页登录管理员账号";
20 [ObservableProperty] public partial string StatusText { get; set; } = "正在确认管理员登录状态...";
20 21 [ObservableProperty] public partial bool IsBusy { get; set; }
21 22
22 23 private string? _originalVersion;
@@ -36,7 +37,7 @@ public partial class AddressResolveManagePageViewModel : ViewModelBase
36 37 [RelayCommand]
37 38 public async Task RefreshAsync()
38 39 {
39 if (!EnsureLoggedIn())
40 if (!await EnsureLoggedInAsync())
40 41 return;
41 42
42 43 IsBusy = true;
@@ -77,7 +78,7 @@ public partial class AddressResolveManagePageViewModel : ViewModelBase
77 78 [RelayCommand]
78 79 private async Task SaveAsync()
79 80 {
80 if (!EnsureLoggedIn())
81 if (!await EnsureLoggedInAsync())
81 82 return;
82 83 if (!TryBuildModel(out var model, out var error))
83 84 {
@@ -115,7 +116,9 @@ public partial class AddressResolveManagePageViewModel : ViewModelBase
115 116 [RelayCommand]
116 117 private async Task RemoveAsync()
117 118 {
118 if (!EnsureLoggedIn() || SelectedItem is null)
119 if (!await EnsureLoggedInAsync())
120 return;
121 if (SelectedItem is null)
119 122 {
120 123 StatusText = "请先选择要删除的版本";
121 124 return;
@@ -146,11 +149,11 @@ public partial class AddressResolveManagePageViewModel : ViewModelBase
146 149 }
147 150 }
148 151
149 private bool EnsureLoggedIn()
152 private async Task<bool> EnsureLoggedInAsync()
150 153 {
151 if (!string.IsNullOrWhiteSpace(DataManager.ClientRequester.Session))
154 if (await AdministratorSessionManager.InitializeAsync())
152 155 return true;
153 StatusText = "请先返回主页登录管理员账号";
156 StatusText = $"{AdministratorSessionManager.StatusText};请前往设置完成管理员登录";
154 157 return false;
155 158 }
156 159
Modified Backend/HaloPixelToolBox.Backend/ViewModels/AppShellPageViewModel.cs +17 -4
@@ -1,4 +1,5 @@
1 using CommunityToolkit.Mvvm.ComponentModel;
1 using CommunityToolkit.Mvvm.ComponentModel;
2 using HaloPixelToolBox.Backend.Utilities;
2 3 using Microsoft.UI.Xaml.Navigation;
3 4 using XFEExtension.NetCore.WinUIHelper.Interface.Services;
4 5 using XFEExtension.NetCore.WinUIHelper.Utilities;
@@ -8,7 +9,7 @@ namespace HaloPixelToolBox.Backend.ViewModels;
8 9 public partial class AppShellPageViewModel : ViewModelBase
9 10 {
10 11 [ObservableProperty] public partial bool CanGoBack { get; set; }
11 [ObservableProperty] public partial string UserName { get; set; } = "默认用户";
12 [ObservableProperty] public partial string UserName { get; set; } = "未登录";
12 13 public IDialogService DialogService { get; set; } = ServiceManager.GetService<IDialogService>();
13 14 public INavigationViewService NavigationViewService { get; set; } = ServiceManager.GetService<INavigationViewService>();
14 15 public IMessageService MessageService { get; set; } = ServiceManager.GetService<IMessageService>();
@@ -17,7 +18,19 @@ public partial class AppShellPageViewModel : ViewModelBase
17 18 public AppShellPageViewModel()
18 19 {
19 20 NavigationViewService.NavigationService.Navigated += NavigationService_Navigated;
21 AdministratorSessionManager.StateChanged += AdministratorSessionManager_StateChanged;
22 _ = InitializeAdministratorSessionAsync();
20 23 }
21 24
22 private void NavigationService_Navigated(object? sender, NavigationEventArgs e) => CanGoBack = NavigationViewService.NavigationService.CanGoBack;
23 }
25 private async Task InitializeAdministratorSessionAsync()
26 {
27 await AdministratorSessionManager.InitializeAsync();
28 UserName = AdministratorSessionManager.DisplayName;
29 }
30
31 private void AdministratorSessionManager_StateChanged(object? sender, EventArgs e) =>
32 UserName = AdministratorSessionManager.DisplayName;
33
34 private void NavigationService_Navigated(object? sender, NavigationEventArgs e) =>
35 CanGoBack = NavigationViewService.NavigationService.CanGoBack;
36 }
Modified Backend/HaloPixelToolBox.Backend/ViewModels/IPBanManagePageViewModel.cs +10 -7
@@ -1,5 +1,6 @@
1 1 using CommunityToolkit.Mvvm.ComponentModel;
2 2 using CommunityToolkit.Mvvm.Input;
3 using HaloPixelToolBox.Backend.Utilities;
3 4 using HaloPixelToolBox.Core.Utilities;
4 5 using System.Collections.ObjectModel;
5 6 using System.Net;
@@ -13,13 +14,13 @@ public partial class IPBanManagePageViewModel : ViewModelBase
13 14 [ObservableProperty] public partial IPAddressInfo? SelectedItem { get; set; }
14 15 [ObservableProperty] public partial string IPAddressText { get; set; } = string.Empty;
15 16 [ObservableProperty] public partial string Notes { get; set; } = string.Empty;
16 [ObservableProperty] public partial string StatusText { get; set; } = "请先在主页登录管理员账号";
17 [ObservableProperty] public partial string StatusText { get; set; } = "正在确认管理员登录状态...";
17 18 [ObservableProperty] public partial bool IsBusy { get; set; }
18 19
19 20 [RelayCommand]
20 21 public async Task RefreshAsync()
21 22 {
22 if (!EnsureLoggedIn())
23 if (!await EnsureLoggedInAsync())
23 24 return;
24 25
25 26 IsBusy = true;
@@ -48,7 +49,7 @@ public partial class IPBanManagePageViewModel : ViewModelBase
48 49 [RelayCommand]
49 50 private async Task AddAsync()
50 51 {
51 if (!EnsureLoggedIn())
52 if (!await EnsureLoggedInAsync())
52 53 return;
53 54 var input = IPAddressText.Trim();
54 55 if (!System.Net.IPAddress.TryParse(input, out var parsedAddress))
@@ -91,7 +92,9 @@ public partial class IPBanManagePageViewModel : ViewModelBase
91 92 [RelayCommand]
92 93 private async Task RemoveAsync()
93 94 {
94 if (!EnsureLoggedIn() || SelectedItem is null)
95 if (!await EnsureLoggedInAsync())
96 return;
97 if (SelectedItem is null)
95 98 {
96 99 StatusText = "请先选择要解封的 IP";
97 100 return;
@@ -122,11 +125,11 @@ public partial class IPBanManagePageViewModel : ViewModelBase
122 125 }
123 126 }
124 127
125 private bool EnsureLoggedIn()
128 private async Task<bool> EnsureLoggedInAsync()
126 129 {
127 if (!string.IsNullOrWhiteSpace(DataManager.ClientRequester.Session))
130 if (await AdministratorSessionManager.InitializeAsync())
128 131 return true;
129 StatusText = "请先返回主页登录管理员账号";
132 StatusText = $"{AdministratorSessionManager.StatusText};请前往设置完成管理员登录";
130 133 return false;
131 134 }
132 135 }
Modified Backend/HaloPixelToolBox.Backend/ViewModels/MainPageViewModel.cs +1 -135
@@ -1,11 +1,3 @@
1 using CommunityToolkit.Mvvm.ComponentModel;
2 using CommunityToolkit.Mvvm.Input;
3 using HaloPixelToolBox.Backend.Profiles.CacheProfiles;
4 using HaloPixelToolBox.Backend.Profiles.CrossVersionProfiles;
5 using HaloPixelToolBox.Core.Models.User;
6 using HaloPixelToolBox.Core.Utilities;
7 using System.Net;
8 using XFEExtension.NetCore.ServerInteractive.Models.RequesterModels;
9 1 using XFEExtension.NetCore.WinUIHelper.Interface.Services;
10 2 using XFEExtension.NetCore.WinUIHelper.Utilities;
11 3
@@ -13,136 +5,10 @@ namespace HaloPixelToolBox.Backend.ViewModels;
13 5
14 6 public partial class MainPageViewModel : ViewModelBase
15 7 {
16 [ObservableProperty] public partial string ServerAddress { get; set; } = SystemProfile.ServerAddress;
17 [ObservableProperty] public partial string Account { get; set; } = CacheProfile.Account;
18 [ObservableProperty] public partial string Password { get; set; } = string.Empty;
19 [ObservableProperty] public partial string StatusText { get; set; } = "尚未连接服务器";
20 [ObservableProperty] public partial bool IsBusy { get; set; }
21 [ObservableProperty] public partial bool IsLoggedIn { get; set; }
22
23 8 public IAutoNavigationParameterService<string> AutoNavigationParameterService { get; set; } = ServiceManager.GetService<IAutoNavigationParameterService<string>>();
24 9
25 public MainPageViewModel()
26 {
10 public MainPageViewModel() =>
27 11 AutoNavigationParameterService.ParameterChange += AutoNavigationParameterService_ParameterChange;
28 _ = RestoreSessionAsync();
29 }
30
31 partial void OnServerAddressChanged(string value) => SystemProfile.ServerAddress = value.Trim();
32 12
33 13 private void AutoNavigationParameterService_ParameterChange(object? sender, string? e) { }
34
35 [RelayCommand]
36 private async Task LoginAsync()
37 {
38 if (string.IsNullOrWhiteSpace(Account) || string.IsNullOrWhiteSpace(Password))
39 {
40 StatusText = "请输入管理员账号和密码";
41 return;
42 }
43
44 IsBusy = true;
45 try
46 {
47 StatusText = "正在连接服务器...";
48 if (!await DataManager.InitializeAsync([ServerAddress], true))
49 {
50 StatusText = "服务器连接失败,请检查地址和服务器状态";
51 return;
52 }
53
54 var response = await DataManager.ClientRequester.Request<UserLoginResult<MyUserFaceInfo>>("login", Account.Trim(), Password);
55 if (response.StatusCode != HttpStatusCode.OK || response.Result?.UserInfo is null)
56 {
57 StatusText = "登录失败,请检查账号或密码";
58 return;
59 }
60
61 if (response.Result.UserInfo.PermissionLevel < (int)UserRole.管理员)
62 {
63 DataManager.ClientRequester.Session = string.Empty;
64 StatusText = "该账号没有管理员权限";
65 return;
66 }
67
68 CacheProfile.Session = response.Result.Session;
69 CacheProfile.Account = Account.Trim();
70 SystemProfile.ServerAddress = ServerAddress.Trim();
71 IsLoggedIn = true;
72 Password = string.Empty;
73 StatusText = $"已登录:{response.Result.UserInfo.NickName}({(UserRole)response.Result.UserInfo.PermissionLevel})";
74 }
75 catch (Exception ex)
76 {
77 StatusText = $"登录失败:{ex.Message}";
78 }
79 finally
80 {
81 IsBusy = false;
82 }
83 }
84
85 [RelayCommand]
86 private async Task ReconnectAsync()
87 {
88 IsBusy = true;
89 try
90 {
91 var connected = await DataManager.InitializeAsync([ServerAddress], true);
92 StatusText = connected ? "服务器连接正常,请登录" : "服务器连接失败";
93 if (connected && !string.IsNullOrWhiteSpace(CacheProfile.Session))
94 await RestoreSessionAsync();
95 }
96 finally
97 {
98 IsBusy = false;
99 }
100 }
101
102 [RelayCommand]
103 private void Logout()
104 {
105 DataManager.ClientRequester.Session = string.Empty;
106 CacheProfile.Session = string.Empty;
107 IsLoggedIn = false;
108 StatusText = "已退出登录";
109 }
110
111 private async Task RestoreSessionAsync()
112 {
113 if (string.IsNullOrWhiteSpace(CacheProfile.Session))
114 return;
115
116 IsBusy = true;
117 try
118 {
119 if (!await DataManager.InitializeAsync([ServerAddress]))
120 {
121 StatusText = "服务器连接失败";
122 return;
123 }
124
125 DataManager.ClientRequester.Session = CacheProfile.Session;
126 var response = await DataManager.ClientRequester.Request<MyUserFaceInfo>("relogin");
127 if (response.StatusCode == HttpStatusCode.OK &&
128 response.Result is { } user &&
129 user.PermissionLevel >= (int)UserRole.管理员)
130 {
131 IsLoggedIn = true;
132 StatusText = $"已恢复登录:{user.NickName}({(UserRole)user.PermissionLevel})";
133 return;
134 }
135
136 Logout();
137 StatusText = "登录已过期,请重新登录";
138 }
139 catch (Exception ex)
140 {
141 StatusText = $"恢复登录失败:{ex.Message}";
142 }
143 finally
144 {
145 IsBusy = false;
146 }
147 }
148 14 }
Modified Backend/HaloPixelToolBox.Backend/ViewModels/SettingPageViewModel.cs +82 -1
@@ -2,7 +2,10 @@
2 2 using CommunityToolkit.Mvvm.ComponentModel;
3 3 using CommunityToolkit.Mvvm.Input;
4 4 using HaloPixelToolBox.Backend.Core.Utilities.Helpers;
5 using HaloPixelToolBox.Backend.Profiles.CacheProfiles;
5 6 using HaloPixelToolBox.Backend.Profiles.CrossVersionProfiles;
7 using HaloPixelToolBox.Backend.Utilities;
8 using HaloPixelToolBox.Core.Utilities;
6 9 using Microsoft.Win32;
7 10 using XFEExtension.NetCore.FileExtension;
8 11 using XFEExtension.NetCore.WinUIHelper.Interface.Services;
@@ -11,8 +14,14 @@ using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
11 14
12 15 namespace HaloPixelToolBox.Backend.ViewModels;
13 16
14 public partial class SettingPageViewModel : ViewModelBase
17 public partial class SettingPageViewModel : ViewModelBase, IDisposable
15 18 {
19 [ObservableProperty] public partial string ServerAddress { get; set; } = SystemProfile.ServerAddress;
20 [ObservableProperty] public partial string Account { get; set; } = CacheProfile.Account;
21 [ObservableProperty] public partial string Password { get; set; } = CacheProfile.Password;
22 [ObservableProperty] public partial string LoginStatusText { get; set; } = AdministratorSessionManager.StatusText;
23 [ObservableProperty] public partial bool IsLoginBusy { get; set; } = AdministratorSessionManager.IsBusy;
24 [ObservableProperty] public partial bool IsLoggedIn { get; set; } = AdministratorSessionManager.IsLoggedIn;
16 25 [ObservableProperty] public partial bool IsAutoStartEnable { get; set; } = SystemProfile.AutoStart;
17 26 [ObservableProperty] public partial string AppCacheDirectory { get; set; } = AppPathHelper.AppCache;
18 27 [ObservableProperty] public partial string AppCacheSize { get; set; } = FileHelper.GetDirectorySize(new(AppPathHelper.AppCache)).FileSize();
@@ -21,6 +30,78 @@ public partial class SettingPageViewModel : ViewModelBase
21 30 public ISettingService SettingService { get; set; } = ServiceManager.GetService<ISettingService>();
22 31 public IDialogService DialogService { get; set; } = ServiceManager.GetService<IDialogService>();
23 32
33 public SettingPageViewModel()
34 {
35 AdministratorSessionManager.StateChanged += AdministratorSessionManager_StateChanged;
36 _ = InitializeAdministratorSessionAsync();
37 }
38
39 private async Task InitializeAdministratorSessionAsync()
40 {
41 await AdministratorSessionManager.InitializeAsync();
42 SynchronizeLoginState();
43 }
44
45 private void AdministratorSessionManager_StateChanged(object? sender, EventArgs e) => SynchronizeLoginState();
46
47 public void Dispose() => AdministratorSessionManager.StateChanged -= AdministratorSessionManager_StateChanged;
48
49 private void SynchronizeLoginState()
50 {
51 LoginStatusText = AdministratorSessionManager.StatusText;
52 IsLoginBusy = AdministratorSessionManager.IsBusy;
53 IsLoggedIn = AdministratorSessionManager.IsLoggedIn;
54 }
55
56 [RelayCommand]
57 private async Task LoginAsync()
58 {
59 if (!TrySaveServerAddress())
60 return;
61
62 if (await AdministratorSessionManager.LoginAsync(Account, Password))
63 {
64 Account = CacheProfile.Account;
65 Password = CacheProfile.Password;
66 }
67
68 SynchronizeLoginState();
69 }
70
71 [RelayCommand]
72 private async Task ReconnectAsync()
73 {
74 if (!TrySaveServerAddress())
75 return;
76
77 await AdministratorSessionManager.InitializeAsync(true);
78 SynchronizeLoginState();
79 }
80
81 [RelayCommand]
82 private async Task LogoutAsync()
83 {
84 await AdministratorSessionManager.LogoutAsync();
85 Password = string.Empty;
86 SynchronizeLoginState();
87 }
88
89 private bool TrySaveServerAddress()
90 {
91 var serverAddress = ServerAddress.Trim().TrimEnd('/');
92 if (!Uri.TryCreate(serverAddress, UriKind.Absolute, out var uri) ||
93 (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
94 {
95 LoginStatusText = "服务器地址无效,请输入完整的 HTTP 或 HTTPS 地址";
96 return false;
97 }
98
99 ServerAddress = serverAddress;
100 SystemProfile.ServerAddress = serverAddress;
101 DataManager.Configure(serverAddress);
102 return true;
103 }
104
24 105 partial void OnIsAutoStartEnableChanged(bool value)
25 106 {
26 107 SystemProfile.AutoStart = value;
Modified Backend/HaloPixelToolBox.Backend/Views/MainPage.xaml +40 -16
@@ -3,28 +3,52 @@
3 3 x:Class="HaloPixelToolBox.Backend.Views.MainPage"
4 4 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5 5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6 xmlns:local="using:HaloPixelToolBox.Backend.Views"
7 6 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8 7 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9 8 mc:Ignorable="d">
10 <Grid MaxWidth="760" HorizontalAlignment="Left">
9 <Grid MaxWidth="960" HorizontalAlignment="Left">
11 10 <StackPanel Spacing="16">
12 11 <TextBlock Text="花再工具箱管理端" FontSize="30" FontWeight="SemiBold"/>
13 <TextBlock Text="登录管理员账号后,可维护动态解析地址和服务器 IP 黑名单。" TextWrapping="Wrap" Opacity="0.72"/>
14 <Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8" Padding="20">
15 <StackPanel Spacing="12">
16 <TextBlock Text="服务器连接" FontSize="20" FontWeight="SemiBold"/>
17 <TextBox Header="服务器地址" Text="{x:Bind ViewModel.ServerAddress, Mode=TwoWay}" PlaceholderText="http://localhost:3300/api"/>
18 <TextBox Header="管理员账号" Text="{x:Bind ViewModel.Account, Mode=TwoWay}"/>
19 <PasswordBox Header="管理员密码" Password="{x:Bind ViewModel.Password, Mode=TwoWay}"/>
20 <StackPanel Orientation="Horizontal" Spacing="10">
21 <Button Content="登录" Command="{x:Bind ViewModel.LoginCommand}"/>
22 <Button Content="重新连接" Command="{x:Bind ViewModel.ReconnectCommand}"/>
23 <Button Content="退出登录" Command="{x:Bind ViewModel.LogoutCommand}"/>
12 <TextBlock Text="集中维护客户端所需的动态解析地址与服务器访问策略。" TextWrapping="Wrap" Opacity="0.72"/>
13
14 <Grid ColumnSpacing="16">
15 <Grid.ColumnDefinitions>
16 <ColumnDefinition Width="*"/>
17 <ColumnDefinition Width="*"/>
18 </Grid.ColumnDefinitions>
19 <Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
20 BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
21 BorderThickness="1" CornerRadius="8" Padding="20">
22 <StackPanel Spacing="8">
23 <FontIcon Glyph="&#xE8B7;" HorizontalAlignment="Left" FontSize="24"/>
24 <TextBlock Text="解析地址管理" FontSize="20" FontWeight="SemiBold"/>
25 <TextBlock Text="按客户端版本维护组件、基址与多级偏移,保存后由服务器动态下发。"
26 TextWrapping="Wrap" Opacity="0.68"/>
27 </StackPanel>
28 </Border>
29 <Border Grid.Column="1"
30 Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
31 BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
32 BorderThickness="1" CornerRadius="8" Padding="20">
33 <StackPanel Spacing="8">
34 <FontIcon Glyph="&#xE72E;" HorizontalAlignment="Left" FontSize="24"/>
35 <TextBlock Text="IP 封禁管理" FontSize="20" FontWeight="SemiBold"/>
36 <TextBlock Text="查看、添加和解除服务器 IP 黑名单,阻止异常客户端继续访问。"
37 TextWrapping="Wrap" Opacity="0.68"/>
38 </StackPanel>
39 </Border>
40 </Grid>
41
42 <Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
43 BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
44 BorderThickness="1" CornerRadius="8" Padding="20">
45 <StackPanel Orientation="Horizontal" Spacing="12">
46 <FontIcon Glyph="&#xE713;" FontSize="22"/>
47 <StackPanel Spacing="4">
48 <TextBlock Text="账号与服务器设置" FontSize="18" FontWeight="SemiBold"/>
49 <TextBlock Text="管理员登录已移至左下角的“设置”;保存凭据后,应用启动时会自动恢复登录。"
50 TextWrapping="Wrap" Opacity="0.68"/>
24 51 </StackPanel>
25 <ProgressBar IsIndeterminate="{x:Bind ViewModel.IsBusy, Mode=OneWay}"/>
26 <TextBlock Text="{x:Bind ViewModel.StatusText, Mode=OneWay}" TextWrapping="Wrap"/>
27 <TextBlock Text="首次运行默认账号为 admin,默认密码为 HaloPixelToolBox@2026;可在启动服务器前通过 HALOPIXEL_ADMIN_USERNAME 和 HALOPIXEL_ADMIN_PASSWORD 环境变量覆盖。" TextWrapping="Wrap" Opacity="0.62"/>
28 52 </StackPanel>
29 53 </Border>
30 54 </StackPanel>
Modified Backend/HaloPixelToolBox.Backend/Views/SettingPage.xaml +56 -9
@@ -3,7 +3,6 @@
3 3 x:Class="HaloPixelToolBox.Backend.Views.SettingPage"
4 4 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5 5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6 xmlns:local="using:HaloPixelToolBox.Views"
7 6 xmlns:controls="using:CommunityToolkit.WinUI.Controls"
8 7 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
9 8 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -13,26 +12,67 @@
13 12 BasedOn="{StaticResource BodyStrongTextBlockStyle}"
14 13 TargetType="TextBlock">
15 14 <Style.Setters>
16 <Setter Property="Margin" Value="1,30,0,6" />
15 <Setter Property="Margin" Value="1,30,0,6"/>
17 16 </Style.Setters>
18 17 </Style>
19 18 </Page.Resources>
20 19
21 20 <Grid>
22 <ContentDialog x:Name="cleanCacheContentDialog" Title="清除应用缓存" Content="是否清除应用的所有缓存文件(可以定期清理)" IsPrimaryButtonEnabled="True" IsSecondaryButtonEnabled="True" PrimaryButtonText="确定" SecondaryButtonText="取消" DefaultButton="Primary"/>
21 <ContentDialog x:Name="cleanCacheContentDialog"
22 Title="清除应用缓存"
23 Content="是否清除应用的所有缓存文件(可以定期清理)?"
24 IsPrimaryButtonEnabled="True"
25 IsSecondaryButtonEnabled="True"
26 PrimaryButtonText="确定"
27 SecondaryButtonText="取消"
28 DefaultButton="Primary"/>
23 29 <ScrollView>
24 <StackPanel Orientation="Vertical" Spacing="4">
25 <TextBlock Text="外观 &amp; 行为" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
30 <StackPanel Orientation="Vertical" Spacing="4" MaxWidth="1000" HorizontalAlignment="Stretch">
31 <TextBlock Text="服务器与管理员" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
32 <controls:SettingsCard Header="服务器地址" Description="管理端请求所使用的 API 地址">
33 <controls:SettingsCard.HeaderIcon>
34 <FontIcon Glyph="&#xE968;"/>
35 </controls:SettingsCard.HeaderIcon>
36 <Grid ColumnSpacing="10">
37 <Grid.ColumnDefinitions>
38 <ColumnDefinition Width="360"/>
39 <ColumnDefinition Width="Auto"/>
40 </Grid.ColumnDefinitions>
41 <TextBox Text="{x:Bind ViewModel.ServerAddress, Mode=TwoWay}" PlaceholderText="https://example.com/api"/>
42 <Button Grid.Column="1" Content="应用并重连" Command="{x:Bind ViewModel.ReconnectCommand}"/>
43 </Grid>
44 </controls:SettingsCard>
45 <controls:SettingsCard Header="管理员登录" Description="{x:Bind ViewModel.LoginStatusText, Mode=OneWay}">
46 <controls:SettingsCard.HeaderIcon>
47 <FontIcon Glyph="&#xE77B;"/>
48 </controls:SettingsCard.HeaderIcon>
49 <StackPanel Width="460" Spacing="10">
50 <TextBox Header="账号"
51 Text="{x:Bind ViewModel.Account, Mode=TwoWay}"/>
52 <PasswordBox Header="密码"
53 Password="{x:Bind ViewModel.Password, Mode=TwoWay}"/>
54 <StackPanel Orientation="Horizontal" Spacing="10">
55 <Button Content="保存并登录" Command="{x:Bind ViewModel.LoginCommand}"/>
56 <Button Content="退出登录" Command="{x:Bind ViewModel.LogoutCommand}"/>
57 <ProgressRing Width="20" Height="20" IsActive="{x:Bind ViewModel.IsLoginBusy, Mode=OneWay}"/>
58 </StackPanel>
59 <TextBlock Text="首次登录成功后会使用当前 Windows 用户加密保存凭据;以后启动应用将自动登录。主动退出会清除已保存的密码。"
60 TextWrapping="Wrap" Opacity="0.62"/>
61 </StackPanel>
62 </controls:SettingsCard>
63
64 <TextBlock Text="外观与行为" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
26 65 <controls:SettingsCard Header="色彩模式" Description="软件显示的主题颜色">
27 66 <controls:SettingsCard.HeaderIcon>
28 67 <FontIcon Glyph="&#xE790;"/>
29 68 </controls:SettingsCard.HeaderIcon>
30 <ComboBox x:Name="appThemeComboBox" Tag="HaloPixelToolBox.Profiles.CrossVersionProfiles.SystemProfile.Theme">
69 <ComboBox x:Name="appThemeComboBox" Tag="HaloPixelToolBox.Backend.Profiles.CrossVersionProfiles.SystemProfile.Theme">
31 70 <ComboBoxItem Content="浅色" Tag="Light"/>
32 71 <ComboBoxItem Content="深色" Tag="Dark"/>
33 72 <ComboBoxItem Content="跟随系统" Tag="Default"/>
34 73 </ComboBox>
35 74 </controls:SettingsCard>
75
36 76 <TextBlock Text="通用" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
37 77 <controls:SettingsCard Header="自启动" Description="开机之后自动启动应用">
38 78 <controls:SettingsCard.HeaderIcon>
@@ -40,8 +80,13 @@
40 80 </controls:SettingsCard.HeaderIcon>
41 81 <ToggleSwitch x:Name="autoStartToggleSwitch" IsOn="{x:Bind ViewModel.IsAutoStartEnable, Mode=TwoWay}"/>
42 82 </controls:SettingsCard>
43 <TextBlock Text="路径 &amp; 存储" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
44 <controls:SettingsCard Header="缓存目录" Description="{x:Bind ViewModel.AppCacheDirectory, Mode=OneWay}" IsClickEnabled="True" Command="{x:Bind ViewModel.OpenPathCommand}" CommandParameter="{x:Bind ViewModel.AppCacheDirectory, Mode=OneWay}">
83
84 <TextBlock Text="路径与存储" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"/>
85 <controls:SettingsCard Header="缓存目录"
86 Description="{x:Bind ViewModel.AppCacheDirectory, Mode=OneWay}"
87 IsClickEnabled="True"
88 Command="{x:Bind ViewModel.OpenPathCommand}"
89 CommandParameter="{x:Bind ViewModel.AppCacheDirectory, Mode=OneWay}">
45 90 <controls:SettingsCard.ActionIcon>
46 91 <FontIcon Glyph="&#xE8A7;"/>
47 92 </controls:SettingsCard.ActionIcon>
@@ -50,7 +95,9 @@
50 95 <ColumnDefinition/>
51 96 <ColumnDefinition/>
52 97 </Grid.ColumnDefinitions>
53 <TextBlock Style="{ThemeResource CaptionTextBlockStyle}" Text="{x:Bind ViewModel.AppCacheSize, Mode=OneWay}" VerticalAlignment="Center"/>
98 <TextBlock Style="{ThemeResource CaptionTextBlockStyle}"
99 Text="{x:Bind ViewModel.AppCacheSize, Mode=OneWay}"
100 VerticalAlignment="Center"/>
54 101 <Button Grid.Column="1" Content="清除缓存" Command="{x:Bind ViewModel.ClearCacheCommand}"/>
55 102 </Grid>
56 103 </controls:SettingsCard>
Modified Backend/HaloPixelToolBox.Backend/Views/SettingPage.xaml.cs +8 -1
@@ -1,4 +1,5 @@
1 1 using HaloPixelToolBox.Backend.ViewModels;
2 using Microsoft.UI.Xaml.Navigation;
2 3 using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
3 4
4 5 namespace HaloPixelToolBox.Backend.Views;
@@ -19,4 +20,10 @@ public sealed partial class SettingPage : Page
19 20 ViewModel.SettingService.Initialize();
20 21 ViewModel.SettingService.RegisterEvents();
21 22 }
22 }
23
24 protected override void OnNavigatedFrom(NavigationEventArgs e)
25 {
26 ViewModel.Dispose();
27 base.OnNavigatedFrom(e);
28 }
29 }
Modified Core/HaloPixelToolBox.Core/Utilities/DataManager.cs +1 -1
@@ -11,7 +11,7 @@ namespace HaloPixelToolBox.Core.Utilities;
11 11 /// </summary>
12 12 public static class DataManager
13 13 {
14 public const string DefaultRequestAddress = "http://localhost:3300/api";
14 public const string DefaultRequestAddress = "http://halopixelbar.api.xfe.studio/api";
15 15
16 16 private static readonly SemaphoreSlim s_initializeLock = new(1, 1);
17 17 private static string[] s_requestAddresses = [DefaultRequestAddress];
Modified Server/HaloPixelToolBox.Server/Program.cs +1 -0