返回提交历史
Modified
Backend/HaloPixelToolBox.Backend/Profiles/CacheProfiles/CacheProfile.cs
+62
-1
Added
Backend/HaloPixelToolBox.Backend/Utilities/AdministratorSessionManager.cs
+219
-0
Added
Backend/HaloPixelToolBox.Backend/Utilities/WindowsDataProtection.cs
+95
-0
Modified
Backend/HaloPixelToolBox.Backend/ViewModels/AddressResolveManagePageViewModel.cs
+10
-7
Modified
Backend/HaloPixelToolBox.Backend/ViewModels/AppShellPageViewModel.cs
+17
-4
Modified
Backend/HaloPixelToolBox.Backend/ViewModels/IPBanManagePageViewModel.cs
+10
-7
Modified
Backend/HaloPixelToolBox.Backend/ViewModels/MainPageViewModel.cs
+1
-135
Modified
Backend/HaloPixelToolBox.Backend/ViewModels/SettingPageViewModel.cs
+82
-1
Modified
Backend/HaloPixelToolBox.Backend/Views/MainPage.xaml
+40
-16
Modified
Backend/HaloPixelToolBox.Backend/Views/SettingPage.xaml
+56
-9
Modified
Backend/HaloPixelToolBox.Backend/Views/SettingPage.xaml.cs
+8
-1
Modified
Core/HaloPixelToolBox.Core/Utilities/DataManager.cs
+1
-1
Modified
Server/HaloPixelToolBox.Server/Program.cs
+1
-0
XFEstudio/HaloPixelToolBox
重构管理员登录与会话管理,支持自动恢复
重构管理端管理员登录与会话管理机制,新增 AdministratorSessionManager 实现自动恢复/重连、凭据本地加密保存(DPAPI)、统一登录状态管理及事件通知。引入 WindowsDataProtection 工具类,CacheProfile 支持加密密码字段。登录/登出逻辑迁移至 SettingPage,主页仅作功能入口。管理页面统一通过会话管理器校验登录状态,提升安全性与体验。优化设置页 UI,支持凭据保存与自动登录,精简主页。调整默认服务器地址,完善事件注册、UI 同步及代码风格。
40064a8
代码差异
13 个文件
+602
-182
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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;
@@ -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="" 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="" 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="" 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>
@@ -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="外观 & 行为" 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=""/>
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=""/>
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=""/>
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="路径 & 存储" 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=""/>
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>
@@ -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
}
@@ -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];