mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
fix: 令牌失效未退出的bug
This commit is contained in:
+19
-13
@@ -157,6 +157,7 @@ class _AppRoot extends ConsumerStatefulWidget {
|
||||
|
||||
class _AppRootState extends ConsumerState<_AppRoot> {
|
||||
late final LoginController _loginController;
|
||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||
final GlobalKey<ScaffoldMessengerState> _messengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
@@ -210,6 +211,15 @@ class _AppRootState extends ConsumerState<_AppRoot> {
|
||||
final auth = ref.watch(authProvider);
|
||||
final appStorage = ref.watch(appStorageProvider);
|
||||
|
||||
ref.listen<bool>(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<String?>(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<Offset>(
|
||||
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<Offset>(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);
|
||||
|
||||
@@ -54,8 +54,9 @@ class _AccountSecurityPageState extends ConsumerState<AccountSecurityPage> {
|
||||
|
||||
Future<void> _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<AccountSecurityPage> {
|
||||
|
||||
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<AccountSecurityPage> {
|
||||
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<AccountSecurityPage> {
|
||||
Future<void> _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<AccountSecurityPage> {
|
||||
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<AccountSecurityPage> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ class _SessionsPageState extends ConsumerState<SessionsPage> {
|
||||
|
||||
Future<void> _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<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
@@ -151,12 +154,21 @@ class _SessionsPageState extends ConsumerState<SessionsPage> {
|
||||
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<SessionsPage> {
|
||||
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<SessionsPage> {
|
||||
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<SessionsPage> {
|
||||
|
||||
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<SessionsPage> {
|
||||
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<SessionsPage> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user