Skip to content

Commit 0f8ebb5

Browse files
committed
feat(认证): 添加基础认证功能支持
在应用设置中添加基础认证配置选项,包括用户名和密码输入 修改Dio客户端以支持基础认证头,并在设置保存时更新认证配置 添加本地存储功能保存认证相关配置
1 parent 36fb841 commit 0f8ebb5

File tree

5 files changed

+192
-6
lines changed

5 files changed

+192
-6
lines changed

lib/core/constants/app_constants.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class AppConstants {
1818
static const String cachedSessionsKey = 'cached_sessions';
1919
static const String currentSessionIdKey = 'current_session_id';
2020

21+
// Basic auth storage keys
22+
static const String basicAuthEnabledKey = 'basic_auth_enabled';
23+
static const String basicAuthUsernameKey = 'basic_auth_username';
24+
static const String basicAuthPasswordKey = 'basic_auth_password';
25+
2126
// Default configuration
2227
static const String defaultTheme = 'system';
2328
static const int maxMessageLength = 10000;

lib/core/di/injection_container.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,18 @@ Future<void> _loadLocalConfig() async {
124124
final baseUrl = 'http://$host:$port';
125125
dioClient.updateBaseUrl(baseUrl);
126126
}
127+
128+
// Load Basic auth configuration
129+
final basicEnabled = await localDataSource.getBasicAuthEnabled();
130+
if (basicEnabled == true) {
131+
final username = await localDataSource.getBasicAuthUsername();
132+
final password = await localDataSource.getBasicAuthPassword();
133+
if ((username != null && username.isNotEmpty) &&
134+
(password != null && password.isNotEmpty)) {
135+
dioClient.setBasicAuth(username, password);
136+
}
137+
} else {
138+
// Ensure no leftover auth header when disabled
139+
dioClient.clearAuth();
140+
}
127141
}

lib/core/network/dio_client.dart

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:dio/dio.dart';
22
import '../constants/api_constants.dart';
3+
import 'dart:convert';
34

45
/// Dio HTTP客户端配置
56
class DioClient {
67
late final Dio _dio;
8+
String? _basicAuthHeader; // cached Authorization header
79

810
DioClient({String? baseUrl}) {
911
_dio = Dio(
@@ -29,13 +31,35 @@ class DioClient {
2931
}
3032
}
3133

34+
/// Set Basic Authorization header using username and password
35+
void setBasicAuth(String username, String password) {
36+
final credentials = '$username:$password';
37+
final encoded = base64Encode(utf8.encode(credentials));
38+
_basicAuthHeader = 'Basic $encoded';
39+
_dio.options.headers[ApiConstants.authorization] = _basicAuthHeader!;
40+
if (const bool.fromEnvironment('dart.vm.product') == false) {
41+
print('[Dio] Basic auth header set');
42+
}
43+
}
44+
45+
/// Clear Authorization header
46+
void clearAuth() {
47+
_basicAuthHeader = null;
48+
_dio.options.headers.remove(ApiConstants.authorization);
49+
if (const bool.fromEnvironment('dart.vm.product') == false) {
50+
print('[Dio] Authorization header cleared');
51+
}
52+
}
53+
3254
void _setupInterceptors() {
3355
_dio.interceptors.add(
3456
LogInterceptor(
57+
requestHeader: true,
3558
requestBody: true,
59+
responseHeader: false,
3660
responseBody: true,
3761
logPrint: (object) {
38-
// 在调试模式下打印日志
62+
// Print logs in debug mode only
3963
if (const bool.fromEnvironment('dart.vm.product') == false) {
4064
print(object);
4165
}
@@ -47,7 +71,11 @@ class DioClient {
4771
_dio.interceptors.add(
4872
InterceptorsWrapper(
4973
onRequest: (options, handler) {
50-
// 可以在这里添加认证头等
74+
// Ensure Authorization header is present if configured
75+
if (_basicAuthHeader != null &&
76+
(options.headers[ApiConstants.authorization] == null)) {
77+
options.headers[ApiConstants.authorization] = _basicAuthHeader;
78+
}
5179
handler.next(options);
5280
},
5381
onResponse: (response, handler) {

lib/data/datasources/app_local_datasource.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ abstract class AppLocalDataSource {
5757
/// 保存会话列表到缓存
5858
Future<void> saveCachedSessions(String sessionsJson);
5959

60+
/// 获取是否启用 Basic 认证
61+
Future<bool?> getBasicAuthEnabled();
62+
63+
/// 保存是否启用 Basic 认证
64+
Future<void> saveBasicAuthEnabled(bool enabled);
65+
66+
/// 获取 Basic 认证用户名
67+
Future<String?> getBasicAuthUsername();
68+
69+
/// 保存 Basic 认证用户名
70+
Future<void> saveBasicAuthUsername(String username);
71+
72+
/// 获取 Basic 认证密码
73+
Future<String?> getBasicAuthPassword();
74+
75+
/// 保存 Basic 认证密码
76+
Future<void> saveBasicAuthPassword(String password);
77+
6078
/// 清除所有数据
6179
Future<void> clearAll();
6280
}
@@ -160,6 +178,36 @@ class AppLocalDataSourceImpl implements AppLocalDataSource {
160178
await sharedPreferences.setString(AppConstants.cachedSessionsKey, sessionsJson);
161179
}
162180

181+
@override
182+
Future<bool?> getBasicAuthEnabled() async {
183+
return sharedPreferences.getBool(AppConstants.basicAuthEnabledKey);
184+
}
185+
186+
@override
187+
Future<void> saveBasicAuthEnabled(bool enabled) async {
188+
await sharedPreferences.setBool(AppConstants.basicAuthEnabledKey, enabled);
189+
}
190+
191+
@override
192+
Future<String?> getBasicAuthUsername() async {
193+
return sharedPreferences.getString(AppConstants.basicAuthUsernameKey);
194+
}
195+
196+
@override
197+
Future<void> saveBasicAuthUsername(String username) async {
198+
await sharedPreferences.setString(AppConstants.basicAuthUsernameKey, username);
199+
}
200+
201+
@override
202+
Future<String?> getBasicAuthPassword() async {
203+
return sharedPreferences.getString(AppConstants.basicAuthPasswordKey);
204+
}
205+
206+
@override
207+
Future<void> saveBasicAuthPassword(String password) async {
208+
await sharedPreferences.setString(AppConstants.basicAuthPasswordKey, password);
209+
}
210+
163211
@override
164212
Future<void> clearAll() async {
165213
await sharedPreferences.clear();

lib/presentation/pages/server_settings_page.dart

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../../core/constants/app_constants.dart';
66
import '../../core/constants/api_constants.dart';
77
import '../../core/di/injection_container.dart';
88
import '../../data/datasources/app_local_datasource.dart';
9+
import '../../core/network/dio_client.dart';
910

1011
/// Server settings page
1112
class ServerSettingsPage extends StatefulWidget {
@@ -19,6 +20,9 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
1920
final _formKey = GlobalKey<FormState>();
2021
final _hostController = TextEditingController();
2122
final _portController = TextEditingController();
23+
final _basicUsernameController = TextEditingController();
24+
final _basicPasswordController = TextEditingController();
25+
bool _basicEnabled = false;
2226

2327
@override
2428
void initState() {
@@ -32,19 +36,30 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
3236
final local = sl<AppLocalDataSource>();
3337
final savedHost = await local.getServerHost();
3438
final savedPort = await local.getServerPort();
39+
final savedBasicEnabled = await local.getBasicAuthEnabled();
40+
final savedUsername = await local.getBasicAuthUsername();
41+
final savedPassword = await local.getBasicAuthPassword();
3542
if (savedHost != null && savedPort != null && mounted) {
3643
_hostController.text = savedHost;
3744
_portController.text = savedPort.toString();
3845
// Keep provider state consistent so other parts reflect the same values
3946
appProvider.setServerConfig(savedHost, savedPort);
4047
}
48+
if (mounted) {
49+
_basicEnabled = savedBasicEnabled ?? false;
50+
_basicUsernameController.text = savedUsername ?? '';
51+
_basicPasswordController.text = savedPassword ?? '';
52+
setState(() {});
53+
}
4154
});
4255
}
4356

4457
@override
4558
void dispose() {
4659
_hostController.dispose();
4760
_portController.dispose();
61+
_basicUsernameController.dispose();
62+
_basicPasswordController.dispose();
4863
super.dispose();
4964
}
5065

@@ -67,13 +82,19 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
6782
),
6883
],
6984
),
85+
resizeToAvoidBottomInset: true,
7086
body: Padding(
7187
padding: const EdgeInsets.all(AppConstants.defaultPadding),
7288
child: Form(
7389
key: _formKey,
74-
child: Column(
75-
crossAxisAlignment: CrossAxisAlignment.stretch,
76-
children: [
90+
child: SingleChildScrollView(
91+
padding: EdgeInsets.only(
92+
bottom: AppConstants.defaultPadding +
93+
MediaQuery.of(context).viewInsets.bottom,
94+
),
95+
child: Column(
96+
crossAxisAlignment: CrossAxisAlignment.stretch,
97+
children: [
7798
// Connection status card
7899
Consumer<AppProvider>(
79100
builder: (context, appProvider, child) {
@@ -195,6 +216,56 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
195216
onPressed: _resetToDefault,
196217
child: const Text('Reset to Default'),
197218
),
219+
220+
const SizedBox(height: AppConstants.defaultPadding),
221+
222+
// Basic authentication section
223+
SwitchListTile(
224+
value: _basicEnabled,
225+
onChanged: (val) {
226+
setState(() {
227+
_basicEnabled = val;
228+
});
229+
},
230+
title: const Text('Enable Basic Authentication'),
231+
subtitle: const Text(
232+
'If enabled, requests will include Basic Authorization header.',
233+
),
234+
),
235+
236+
if (_basicEnabled) ...[
237+
const SizedBox(height: AppConstants.smallPadding),
238+
TextFormField(
239+
controller: _basicUsernameController,
240+
decoration: const InputDecoration(
241+
labelText: 'Username',
242+
prefixIcon: Icon(Icons.person_outline),
243+
),
244+
validator: (value) {
245+
if (!_basicEnabled) return null;
246+
if (value == null || value.isEmpty) {
247+
return 'Please enter username';
248+
}
249+
return null;
250+
},
251+
),
252+
const SizedBox(height: AppConstants.smallPadding),
253+
TextFormField(
254+
controller: _basicPasswordController,
255+
decoration: const InputDecoration(
256+
labelText: 'Password',
257+
prefixIcon: Icon(Icons.lock_outline),
258+
),
259+
obscureText: true,
260+
validator: (value) {
261+
if (!_basicEnabled) return null;
262+
if (value == null || value.isEmpty) {
263+
return 'Please enter password';
264+
}
265+
return null;
266+
},
267+
),
268+
],
198269
],
199270
),
200271
),
@@ -231,6 +302,7 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
231302
],
232303
),
233304
],
305+
),
234306
),
235307
),
236308
),
@@ -256,12 +328,31 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
256328
final success = await appProvider.updateServerConfig(mappedHost, port);
257329

258330
if (success && mounted) {
331+
// Save Basic auth settings
332+
final local = sl<AppLocalDataSource>();
333+
await local.saveBasicAuthEnabled(_basicEnabled);
334+
await local.saveBasicAuthUsername(_basicUsernameController.text.trim());
335+
await local.saveBasicAuthPassword(_basicPasswordController.text.trim());
336+
337+
// Apply to Dio client
338+
final dioClient = sl<DioClient>();
339+
if (_basicEnabled &&
340+
_basicUsernameController.text.trim().isNotEmpty &&
341+
_basicPasswordController.text.trim().isNotEmpty) {
342+
dioClient.setBasicAuth(
343+
_basicUsernameController.text.trim(),
344+
_basicPasswordController.text.trim(),
345+
);
346+
} else {
347+
dioClient.clearAuth();
348+
}
349+
259350
final info = 'Settings saved: http://$mappedHost:$port';
260351
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(info)));
261352
if (mappedHost != host && mounted) {
262353
ScaffoldMessenger.of(context).showSnackBar(
263354
const SnackBar(
264-
content: Text('Android emulator detected: mapped localhost to 10.0.2.2'),
355+
content: Text('Android emulator detected: mapped localhost to 10.0.2.2'),
265356
),
266357
);
267358
}

0 commit comments

Comments
 (0)