From ac68a53e78b5b80669521f970fbf8333e2521785 Mon Sep 17 00:00:00 2001 From: chaoszhu Date: Sun, 5 Jul 2026 16:43:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=A4=E7=89=8C=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E6=9C=AA=E9=80=80=E5=87=BA=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- native/lib/app.dart | 32 +++++---- .../settings/account_security_page.dart | 67 +++++++---------- .../lib/features/settings/sessions_page.dart | 71 ++++++++++--------- 3 files changed, 82 insertions(+), 88 deletions(-) diff --git a/native/lib/app.dart b/native/lib/app.dart index 0c6964f..598c721 100644 --- a/native/lib/app.dart +++ b/native/lib/app.dart @@ -157,6 +157,7 @@ class _AppRoot extends ConsumerStatefulWidget { class _AppRootState extends ConsumerState<_AppRoot> { late final LoginController _loginController; + final GlobalKey _navigatorKey = GlobalKey(); final GlobalKey _messengerKey = GlobalKey(); @@ -210,6 +211,15 @@ class _AppRootState extends ConsumerState<_AppRoot> { final auth = ref.watch(authProvider); final appStorage = ref.watch(appStorageProvider); + ref.listen(authProvider.select((state) => state.signedIn), ( + previous, + next, + ) { + if (previous == true && !next) { + _navigatorKey.currentState?.popUntil((route) => route.isFirst); + } + }); + // Show a SnackBar whenever the 401/403 interceptor stashes a reason, then // clear it so the same message isn't shown twice on rebuilds. ref.listen(signOutReasonProvider, (_, next) { @@ -249,6 +259,7 @@ class _AppRootState extends ConsumerState<_AppRoot> { return MaterialApp( title: 'EasyNode', + navigatorKey: _navigatorKey, scaffoldMessengerKey: _messengerKey, themeMode: ref.watch(themeModeProvider), theme: ThemeData( @@ -321,23 +332,18 @@ class _BrandedSplashGateState extends State<_BrandedSplashGate> vsync: this, duration: const Duration(milliseconds: 850), ); - _logoScale = CurvedAnimation( - parent: _controller, - curve: Curves.elasticOut, - ); + _logoScale = CurvedAnimation(parent: _controller, curve: Curves.elasticOut); _logoFade = CurvedAnimation( parent: _controller, curve: const Interval(0, 0.45, curve: Curves.easeOut), ); - _textOffset = Tween( - begin: const Offset(0, 0.35), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.18, 0.7, curve: Curves.easeOutBack), - ), - ); + _textOffset = Tween(begin: const Offset(0, 0.35), end: Offset.zero) + .animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.18, 0.7, curve: Curves.easeOutBack), + ), + ); unawaited(_controller.forward()); _timer = Timer(const Duration(milliseconds: 1250), () { if (mounted) setState(() => _showSplash = false); diff --git a/native/lib/features/settings/account_security_page.dart b/native/lib/features/settings/account_security_page.dart index 2234c18..e77409d 100644 --- a/native/lib/features/settings/account_security_page.dart +++ b/native/lib/features/settings/account_security_page.dart @@ -54,8 +54,9 @@ class _AccountSecurityPageState extends ConsumerState { Future _loadMfa() async { try { - final enabled = - await ref.read(settingsRepositoryProvider).getMfa2Status(); + final enabled = await ref + .read(settingsRepositoryProvider) + .getMfa2Status(); if (!mounted) return; setState(() { _mfaEnabled = enabled; @@ -101,7 +102,9 @@ class _AccountSecurityPageState extends ConsumerState { setState(() => _saving = true); try { - await ref.read(settingsRepositoryProvider).updateAccount( + await ref + .read(settingsRepositoryProvider) + .updateAccount( oldLoginName: oldName, oldPwd: oldPwd, newLoginName: newName, @@ -110,6 +113,8 @@ class _AccountSecurityPageState extends ConsumerState { if (!mounted) return; _showSnack(l.tr('account.changed')); await ref.read(authProvider.notifier).signOut(); + } on UnauthorizedFailure { + await ref.read(authProvider.notifier).signOut(); } on ApiFailure catch (err) { if (!mounted) return; _showSnack(err.message); @@ -124,8 +129,7 @@ class _AccountSecurityPageState extends ConsumerState { Future _startEnableMfa() async { setState(() => _enableLoading = true); try { - final setup = - await ref.read(settingsRepositoryProvider).getMfa2QrInfo(); + final setup = await ref.read(settingsRepositoryProvider).getMfa2QrInfo(); if (!mounted) return; setState(() => _setup = setup); } on ApiFailure catch (err) { @@ -221,8 +225,7 @@ class _AccountSecurityPageState extends ConsumerState { child: Text(l.tr('common.cancel')), ), FilledButton( - onPressed: () => - Navigator.of(ctx).pop(controller.text.trim()), + onPressed: () => Navigator.of(ctx).pop(controller.text.trim()), child: Text(confirmText), ), ], @@ -274,12 +277,10 @@ class _AccountSecurityPageState extends ConsumerState { showNewPwd: _showNewPwd, showNewPwdConfirm: _showNewPwdConfirm, saving: _saving, - onToggleOldPwd: () => - setState(() => _showOldPwd = !_showOldPwd), - onToggleNewPwd: () => - setState(() => _showNewPwd = !_showNewPwd), - onToggleNewPwdConfirm: () => setState( - () => _showNewPwdConfirm = !_showNewPwdConfirm), + onToggleOldPwd: () => setState(() => _showOldPwd = !_showOldPwd), + onToggleNewPwd: () => setState(() => _showNewPwd = !_showNewPwd), + onToggleNewPwdConfirm: () => + setState(() => _showNewPwdConfirm = !_showNewPwdConfirm), onSubmit: _saveAccount, ), const SizedBox(height: 22), @@ -390,10 +391,7 @@ class _CredentialsCard extends StatelessWidget { controller: oldPwdCtrl, hintText: l.tr('account.oldPwd'), obscure: !showOldPwd, - suffix: _EyeButton( - visible: showOldPwd, - onTap: onToggleOldPwd, - ), + suffix: _EyeButton(visible: showOldPwd, onTap: onToggleOldPwd), validator: (v) => (v == null || v.isEmpty) ? l.tr('account.pwdRequired') : null, @@ -417,10 +415,7 @@ class _CredentialsCard extends StatelessWidget { controller: newPwdCtrl, hintText: l.tr('account.newPwd'), obscure: !showNewPwd, - suffix: _EyeButton( - visible: showNewPwd, - onTap: onToggleNewPwd, - ), + suffix: _EyeButton(visible: showNewPwd, onTap: onToggleNewPwd), validator: (v) => (v == null || v.isEmpty) ? l.tr('account.pwdRequired') : null, @@ -543,22 +538,18 @@ class _TextInput extends StatelessWidget { controller: controller, obscureText: obscure, validator: validator, - style: TextStyle( - fontSize: 14, - color: context.colors.text, - ), + style: TextStyle(fontSize: 14, color: context.colors.text), decoration: InputDecoration( hintText: hintText, - hintStyle: TextStyle( - fontSize: 13, - color: context.colors.softMuted, - ), + hintStyle: TextStyle(fontSize: 13, color: context.colors.softMuted), isDense: true, filled: true, fillColor: context.colors.canvas, suffixIcon: suffix, - contentPadding: - const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: context.colors.border), @@ -799,8 +790,8 @@ class _MfaCardState extends State<_MfaCard> { onPressed: widget.enableLoading ? null : () => widget.onSubmitEnable( - _codeCtrl.text.trim(), - ), + _codeCtrl.text.trim(), + ), style: FilledButton.styleFrom( backgroundColor: context.colors.primary, foregroundColor: context.colors.fontOnPrimary, @@ -878,14 +869,8 @@ class _MfaStatusChip extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), child: Text( - enabled - ? l.tr('account.mfa.statusOn') - : l.tr('account.mfa.statusOff'), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: fg, - ), + enabled ? l.tr('account.mfa.statusOn') : l.tr('account.mfa.statusOff'), + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: fg), ), ); } diff --git a/native/lib/features/settings/sessions_page.dart b/native/lib/features/settings/sessions_page.dart index 7689d45..4177b09 100644 --- a/native/lib/features/settings/sessions_page.dart +++ b/native/lib/features/settings/sessions_page.dart @@ -127,6 +127,9 @@ class _SessionsPageState extends ConsumerState { Future _revoke(LoginSession session) async { final l = AppLocalizations.of(context); + final currentDeviceId = ref.read(authProvider).session?.deviceId ?? ''; + final revokingCurrentSession = + session.deviceId.isNotEmpty && session.deviceId == currentDeviceId; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( @@ -151,12 +154,21 @@ class _SessionsPageState extends ConsumerState { if (confirmed != true || !mounted) return; setState(() => _revokingId = session.id); try { - await ref - .read(settingsRepositoryProvider) - .revokeSession(session.id); + await ref.read(settingsRepositoryProvider).revokeSession(session.id); if (!mounted) return; + if (revokingCurrentSession) { + await ref.read(authProvider.notifier).signOut(); + return; + } _showSnack(l.tr('sessions.revokeDone')); await ref.read(loginLogProvider.notifier).refresh(); + } on UnauthorizedFailure catch (err) { + if (!mounted) return; + if (!revokingCurrentSession) { + _showSnack(err.message); + return; + } + await ref.read(authProvider.notifier).signOut(); } on ApiFailure catch (err) { if (!mounted) return; _showSnack(err.message); @@ -204,10 +216,7 @@ class _SessionsPageState extends ConsumerState { color: context.colors.danger, ), ) - : Icon( - Icons.delete_outline, - color: context.colors.danger, - ), + : Icon(Icons.delete_outline, color: context.colors.danger), ), const SizedBox(width: 6), ], @@ -215,8 +224,7 @@ class _SessionsPageState extends ConsumerState { body: RefreshIndicator( onRefresh: () => runRefreshWithFeedback( context, - () => - ref.read(loginLogProvider.notifier).refresh(throwOnError: true), + () => ref.read(loginLogProvider.notifier).refresh(throwOnError: true), ), child: logAsync.when( loading: () => const Center(child: CircularProgressIndicator()), @@ -235,8 +243,7 @@ class _SessionsPageState extends ConsumerState { Widget _buildBody(LoginLogData data) { final l = AppLocalizations.of(context); - final currentDeviceId = - ref.watch(authProvider).session?.deviceId ?? ''; + final currentDeviceId = ref.watch(authProvider).session?.deviceId ?? ''; return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ @@ -247,9 +254,7 @@ class _SessionsPageState extends ConsumerState { dirty: _whitelistDirty, onAdd: _addIp, onRemove: _removeIp, - onSave: _whitelistDirty && !_whitelistSaving - ? _saveWhitelist - : null, + onSave: _whitelistDirty && !_whitelistSaving ? _saveWhitelist : null, ), const SizedBox(height: 18), _SectionHeader( @@ -264,14 +269,13 @@ class _SessionsPageState extends ConsumerState { padding: const EdgeInsets.only(bottom: 10), child: _SessionCard( session: session, - isCurrent: session.deviceId.isNotEmpty && + isCurrent: + session.deviceId.isNotEmpty && session.deviceId == currentDeviceId, createLabel: _formatTimestamp(session.createAt), expireLabel: _formatTimestamp(session.expireAt), revoking: _revokingId == session.id, - onRevoke: session.revoked - ? null - : () => _revoke(session), + onRevoke: session.revoked ? null : () => _revoke(session), ), ), ], @@ -345,10 +349,7 @@ class _IpWhitelistCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( l.tr('sessions.ipEmpty'), - style: TextStyle( - fontSize: 12, - color: context.colors.softMuted, - ), + style: TextStyle(fontSize: 12, color: context.colors.softMuted), ), ) else @@ -547,11 +548,12 @@ class _SessionCard extends StatelessWidget { final l = AppLocalizations.of(context); final nativeClient = session.isNativeClient; final bg = isCurrent ? context.colors.accentSoft : context.colors.card; - final borderColor = isCurrent ? context.colors.primary : context.colors.border; + final borderColor = isCurrent + ? context.colors.primary + : context.colors.border; final borderWidth = isCurrent ? 2.0 : 1.0; - final ipLabel = - session.ip.isEmpty ? '-' : session.ip; + final ipLabel = session.ip.isEmpty ? '-' : session.ip; final location = session.location; return Container( @@ -568,9 +570,7 @@ class _SessionCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( - nativeClient - ? Icons.smartphone_rounded - : Icons.monitor_rounded, + nativeClient ? Icons.smartphone_rounded : Icons.monitor_rounded, size: 18, color: context.colors.primary, ), @@ -600,10 +600,7 @@ class _SessionCard extends StatelessWidget { ], ), ), - _StatusBadge( - label: _statusLabel(l), - tone: _statusTone(), - ), + _StatusBadge(label: _statusLabel(l), tone: _statusTone()), ], ), const SizedBox(height: 10), @@ -696,8 +693,14 @@ class _StatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { final (bg, fg) = switch (tone) { - _StatusTone.success => (context.colors.success, context.colors.fontOnPrimary), - _StatusTone.primary => (context.colors.accentSoft, context.colors.primary), + _StatusTone.success => ( + context.colors.success, + context.colors.fontOnPrimary, + ), + _StatusTone.primary => ( + context.colors.accentSoft, + context.colors.primary, + ), _StatusTone.muted => (context.colors.chip, context.colors.muted), }; return Container(