fix: unify bridge auth token for desktop connect
This commit is contained in:
parent
b7a842fce3
commit
20257f392e
@ -5,6 +5,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'desktop_client.dart';
|
||||
import 'desktop_input_handler.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../runtime/gateway_acp_client.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
|
||||
@ -131,7 +132,7 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
final display = _displayController.text.trim();
|
||||
int width = int.tryParse(_widthController.text) ?? 1280;
|
||||
int height = int.tryParse(_heightController.text) ?? 720;
|
||||
|
||||
|
||||
if (_adaptiveResolution) {
|
||||
final viewportSize = _getViewportSize();
|
||||
if (viewportSize.width > 0 && viewportSize.height > 0) {
|
||||
@ -141,7 +142,7 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
_heightController.text = height.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final fps = int.tryParse(_fpsController.text) ?? 30;
|
||||
final bitrate = int.tryParse(_bitrateController.text) ?? 2000;
|
||||
_remoteDesktopSize = Size(width.toDouble(), height.toDouble());
|
||||
@ -155,6 +156,21 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
bitrate: bitrate,
|
||||
useGpu: _useGpu,
|
||||
);
|
||||
} on GatewayAcpException catch (error) {
|
||||
if (mounted) {
|
||||
final message = _desktopConnectionErrorMessage(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
appText(
|
||||
'连接AI工作空间失败: $message',
|
||||
'Failed to connect AI Workspace: $message',
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.redAccent,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -170,6 +186,17 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
}
|
||||
}
|
||||
|
||||
String _desktopConnectionErrorMessage(GatewayAcpException error) {
|
||||
final code = (error.code ?? '').trim().toUpperCase();
|
||||
if (code == 'ACP_HTTP_401' || code == 'ACP_HTTP_403') {
|
||||
return appText(
|
||||
'Bridge 认证已过期或被拒绝,请点击“重新同步”后再连接。',
|
||||
'Bridge authorization expired or was rejected. Please re-sync, then connect again.',
|
||||
);
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
Size _getViewportSize() {
|
||||
final renderBox =
|
||||
_viewportKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
@ -194,114 +221,117 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Connection Button
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleConnection,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _connectionState == 'connected'
|
||||
? Colors.redAccent
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orangeAccent
|
||||
: theme.colorScheme.primary),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
_connectionState == 'connected'
|
||||
? Icons.portable_wifi_off_rounded
|
||||
: Icons.settings_remote_rounded,
|
||||
),
|
||||
label: Text(
|
||||
_connectionState == 'connected'
|
||||
? appText('断开连接', 'Disconnect')
|
||||
: (_connectionState == 'connecting'
|
||||
? appText('正在连接...', 'Connecting...')
|
||||
: appText('连接AI工作空间', 'Connect AI Workspace')),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
// Status Indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green.withValues(alpha: 0.15)
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange.withValues(alpha: 0.15)
|
||||
: Colors.grey.withValues(alpha: 0.15)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Connection Button
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleConnection,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _connectionState == 'connected'
|
||||
? Colors.redAccent
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange
|
||||
: Colors.grey),
|
||||
width: 1,
|
||||
? Colors.orangeAccent
|
||||
: theme.colorScheme.primary),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
_connectionState == 'connected'
|
||||
? Icons.portable_wifi_off_rounded
|
||||
: Icons.settings_remote_rounded,
|
||||
),
|
||||
label: Text(
|
||||
_connectionState == 'connected'
|
||||
? appText('断开连接', 'Disconnect')
|
||||
: (_connectionState == 'connecting'
|
||||
? appText('正在连接...', 'Connecting...')
|
||||
: appText(
|
||||
'连接AI工作空间',
|
||||
'Connect AI Workspace',
|
||||
)),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange
|
||||
: Colors.grey),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_connectionState == 'connected'
|
||||
? appText('已连接', 'Connected')
|
||||
// Status Indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green.withValues(alpha: 0.15)
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange.withValues(alpha: 0.15)
|
||||
: Colors.grey.withValues(alpha: 0.15)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
: (_connectionState == 'connecting'
|
||||
? appText('连接中', 'Connecting')
|
||||
: appText('已断开', 'Disconnected')),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange
|
||||
: (isDark
|
||||
? Colors.white70
|
||||
: Colors.black87)),
|
||||
),
|
||||
? Colors.orange
|
||||
: Colors.grey),
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange
|
||||
: Colors.grey),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_connectionState == 'connected'
|
||||
? appText('已连接', 'Connected')
|
||||
: (_connectionState == 'connecting'
|
||||
? appText('连接中', 'Connecting')
|
||||
: appText('已断开', 'Disconnected')),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _connectionState == 'connected'
|
||||
? Colors.green
|
||||
: (_connectionState == 'connecting'
|
||||
? Colors.orange
|
||||
: (isDark
|
||||
? Colors.white70
|
||||
: Colors.black87)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Advanced Options Toggle
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(
|
||||
() => _showAdvancedOptions = !_showAdvancedOptions,
|
||||
// Advanced Options Toggle
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(
|
||||
() => _showAdvancedOptions = !_showAdvancedOptions,
|
||||
),
|
||||
icon: Icon(
|
||||
_showAdvancedOptions
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
),
|
||||
label: const Text('高级选项'),
|
||||
),
|
||||
icon: Icon(
|
||||
_showAdvancedOptions
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
),
|
||||
label: const Text('高级选项'),
|
||||
),
|
||||
// Maximize Toggle
|
||||
if (widget.onToggleMaximize != null)
|
||||
IconButton(
|
||||
@ -315,104 +345,120 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
// Collapse Toggle
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _showControlPanel = false),
|
||||
onPressed: () =>
|
||||
setState(() => _showControlPanel = false),
|
||||
icon: const Icon(Icons.expand_less),
|
||||
tooltip: '折叠面板',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showAdvancedOptions) ...[
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Display Selector
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: _displayController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Display',
|
||||
prefixIcon: Icon(Icons.monitor_rounded, size: 16),
|
||||
if (_showAdvancedOptions) ...[
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Display Selector
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: _displayController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Display',
|
||||
prefixIcon: Icon(
|
||||
Icons.monitor_rounded,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Adaptive Resolution Toggle
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(appText('自适应分辨率', 'Adaptive Resolution')),
|
||||
Switch(
|
||||
value: _adaptiveResolution,
|
||||
onChanged: _connectionState == 'disconnected'
|
||||
? (val) => setState(() => _adaptiveResolution = val)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Resolution settings
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _widthController,
|
||||
enabled: _connectionState == 'disconnected' && !_adaptiveResolution,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '宽度'),
|
||||
// Adaptive Resolution Toggle
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(appText('自适应分辨率', 'Adaptive Resolution')),
|
||||
Switch(
|
||||
value: _adaptiveResolution,
|
||||
onChanged: _connectionState == 'disconnected'
|
||||
? (val) => setState(
|
||||
() => _adaptiveResolution = val,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
enabled: _connectionState == 'disconnected' && !_adaptiveResolution,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '高度'),
|
||||
),
|
||||
),
|
||||
// FPS / Bitrate
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: TextField(
|
||||
controller: _fpsController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '帧率'),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _bitrateController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '码率 (kbps)',
|
||||
// Resolution settings
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _widthController,
|
||||
enabled:
|
||||
_connectionState == 'disconnected' &&
|
||||
!_adaptiveResolution,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '宽度',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// GPU accelerator toggle
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('GPU 加速'),
|
||||
Switch(
|
||||
value: _useGpu,
|
||||
onChanged: _connectionState == 'disconnected'
|
||||
? (val) => setState(() => _useGpu = val)
|
||||
: null,
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
enabled:
|
||||
_connectionState == 'disconnected' &&
|
||||
!_adaptiveResolution,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '高度',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// FPS / Bitrate
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: TextField(
|
||||
controller: _fpsController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '帧率',
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: TextField(
|
||||
controller: _bitrateController,
|
||||
enabled: _connectionState == 'disconnected',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '码率 (kbps)',
|
||||
),
|
||||
),
|
||||
),
|
||||
// GPU accelerator toggle
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('GPU 加速'),
|
||||
Switch(
|
||||
value: _useGpu,
|
||||
onChanged: _connectionState == 'disconnected'
|
||||
? (val) => setState(() => _useGpu = val)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (!_showControlPanel)
|
||||
Align(
|
||||
@ -427,8 +473,7 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
),
|
||||
|
||||
if (_showControlPanel)
|
||||
const SizedBox(height: 16),
|
||||
if (_showControlPanel) const SizedBox(height: 16),
|
||||
|
||||
// Stream Viewport Card
|
||||
Expanded(
|
||||
@ -516,7 +561,8 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
},
|
||||
child: RTCVideoView(
|
||||
_localRenderer,
|
||||
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
objectFit: RTCVideoViewObjectFit
|
||||
.RTCVideoViewObjectFitContain,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
@ -541,8 +587,14 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_connectionState == 'connecting'
|
||||
? appText('正在建立 WebRTC 连接,请稍候...', 'Establishing WebRTC connection, please wait...')
|
||||
: appText('未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。', 'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.'),
|
||||
? appText(
|
||||
'正在建立 WebRTC 连接,请稍候...',
|
||||
'Establishing WebRTC connection, please wait...',
|
||||
)
|
||||
: appText(
|
||||
'未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。',
|
||||
'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.',
|
||||
),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
|
||||
@ -64,7 +64,8 @@ Future<Map<String, dynamic>> loadBridgeMetadataForSettingsAbout({
|
||||
response.statusCode == HttpStatus.forbidden) {
|
||||
return const <String, dynamic>{
|
||||
'status': 'unauthorized',
|
||||
'message': 'Bridge authorization rejected',
|
||||
'message':
|
||||
'Bridge authorization rejected. Please re-sync the account token.',
|
||||
'version': '',
|
||||
'commit': '',
|
||||
'image': '',
|
||||
@ -86,7 +87,8 @@ Future<Map<String, dynamic>> loadBridgeMetadataForSettingsAbout({
|
||||
if (decoded is Map) {
|
||||
return decoded.cast<String, dynamic>();
|
||||
}
|
||||
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error: $e\n$stackTrace');
|
||||
return const <String, dynamic>{
|
||||
'status': 'unavailable',
|
||||
'version': '',
|
||||
@ -296,13 +298,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
await controller.refreshSingleAgentCapabilitiesInternal(
|
||||
forceRefresh: true,
|
||||
);
|
||||
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error: $e\n$stackTrace');
|
||||
// Best effort only. Account sync should still succeed if runtime refresh
|
||||
// is temporarily unavailable.
|
||||
}
|
||||
try {
|
||||
await controller.refreshAcpCapabilitiesInternal(forceRefresh: true);
|
||||
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error: $e\n$stackTrace');
|
||||
// Best effort only. Runtime capabilities can be retried later.
|
||||
}
|
||||
}
|
||||
@ -400,7 +404,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
if (status == 'unauthorized') {
|
||||
await widget.controller.settingsController
|
||||
.markAccountBridgeRuntimeUnavailable('Bridge authorization rejected');
|
||||
.markAccountBridgeRuntimeUnavailable(
|
||||
'Bridge token expired or rejected. Please re-sync the account token.',
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -576,10 +582,10 @@ class _SettingsTabSelector extends StatelessWidget {
|
||||
tab == SettingsTab.remoteDesktop
|
||||
? Icons.desktop_windows_outlined
|
||||
: (tab == SettingsTab.logs
|
||||
? Icons.terminal_outlined
|
||||
: (tab == SettingsTab.archivedTasks
|
||||
? Icons.inventory_2_outlined
|
||||
: Icons.hub_outlined)),
|
||||
? Icons.terminal_outlined
|
||||
: (tab == SettingsTab.archivedTasks
|
||||
? Icons.inventory_2_outlined
|
||||
: Icons.hub_outlined)),
|
||||
),
|
||||
label: Text(tab.label),
|
||||
),
|
||||
|
||||
@ -50,6 +50,13 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
.clamp(0, kGatewayProfileListLength - 1);
|
||||
|
||||
if (resolvedProfileIndex == kGatewayRemoteProfileIndex) {
|
||||
final managedBridgeToken = (await storeInternal.loadAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
))?.trim();
|
||||
if (managedBridgeToken?.isNotEmpty == true) {
|
||||
return managedBridgeToken!;
|
||||
}
|
||||
|
||||
final effective = snapshotInternal.acpBridgeServerModeConfig.effective;
|
||||
if (effective.tokenRef.isNotEmpty) {
|
||||
final token = await loadSecretValueByRef(effective.tokenRef);
|
||||
|
||||
@ -127,7 +127,10 @@ void main() {
|
||||
);
|
||||
|
||||
expect(metadata['status'], 'unauthorized');
|
||||
expect(metadata['message'], 'Bridge authorization rejected');
|
||||
expect(
|
||||
metadata['message'],
|
||||
'Bridge authorization rejected. Please re-sync the account token.',
|
||||
);
|
||||
expect(metadata['version'], '');
|
||||
expect(metadata['commit'], '');
|
||||
expect(metadata['image'], '');
|
||||
|
||||
@ -10,6 +10,78 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
group('SettingsController account sync', () {
|
||||
test(
|
||||
'prefers managed bridge token over stale profile token for remote gateway auth',
|
||||
() async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-account-sync-token-precedence-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await storeRoot.exists()) {
|
||||
await storeRoot.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
final store = SecureConfigStore(
|
||||
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
||||
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
||||
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
||||
enableSecureStorage: false,
|
||||
);
|
||||
await store.initialize();
|
||||
await store.saveSettingsSnapshot(
|
||||
SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||
.copyWith(
|
||||
selfHosted: const AcpBridgeServerSelfHostedConfig(
|
||||
serverUrl: 'https://xworkmate-bridge.svc.plus',
|
||||
username: 'ubuntu',
|
||||
passwordRef: 'legacy.bridge.token',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await store.saveSecretValueByRef('legacy.bridge.token', 'stale-token');
|
||||
await store.saveAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
value: 'managed-token',
|
||||
);
|
||||
await store.saveAccountSessionToken('session-token');
|
||||
await store.saveAccountSessionSummary(
|
||||
const AccountSessionSummary(
|
||||
userId: 'user-1',
|
||||
email: 'review@svc.plus',
|
||||
name: 'Review User',
|
||||
role: 'reviewer',
|
||||
mfaEnabled: true,
|
||||
),
|
||||
);
|
||||
await store.saveAccountSyncState(
|
||||
AccountSyncState.defaults().copyWith(
|
||||
syncState: 'ready',
|
||||
tokenConfigured: const AccountTokenConfigured(
|
||||
bridge: true,
|
||||
vault: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await controller.settingsControllerInternal.initialize();
|
||||
|
||||
final token = await controller.settingsControllerInternal
|
||||
.loadEffectiveGatewayToken(
|
||||
profileIndex: kGatewayRemoteProfileIndex,
|
||||
);
|
||||
|
||||
expect(token, 'managed-token');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'updates in-memory blocked state when bridge authorization is unavailable',
|
||||
() async {
|
||||
@ -495,7 +567,7 @@ void main() {
|
||||
|
||||
await controller.settingsControllerInternal
|
||||
.markAccountBridgeRuntimeUnavailable(
|
||||
'Bridge authorization rejected',
|
||||
'Bridge token expired or rejected. Please re-sync the account token.',
|
||||
);
|
||||
|
||||
expect(controller.isBridgeAcpRuntimeConfiguredInternal(), isFalse);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user