🐛 Fix refresh token

This commit is contained in:
LittleSheep 2024-11-09 19:32:21 +08:00
parent 4d12d243b3
commit a629f5e12c
6 changed files with 177 additions and 40 deletions

View File

@ -35,5 +35,13 @@
"loginEnterPassword": "Enter the code", "loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}", "loginSuccess": "Logged in as {}",
"authFactorPassword": "Password", "authFactorPassword": "Password",
"authFactorEmail": "Email verification code" "authFactorEmail": "Email verification code",
"accountIntroTitle": "Hello there!",
"accountIntroSubtitle": "Pick an option below to get started.",
"accountLogout": "Logout",
"accountLogoutSubtitle": "Log out of the current account.",
"accountLogoutConfirmTitle": "Are you sure you want to logout?",
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
"accountPublishers": "Your publishers",
"accountPublishersSubtitle": "Manage your publish identities."
} }

View File

@ -35,5 +35,13 @@
"loginEnterPassword": "验证代码", "loginEnterPassword": "验证代码",
"loginSuccess": "登录为 {}", "loginSuccess": "登录为 {}",
"authFactorPassword": "密码", "authFactorPassword": "密码",
"authFactorEmail": "电邮一次性验证码" "authFactorEmail": "电邮一次性验证码",
"accountIntroTitle": "喜欢您来!",
"accountIntroSubtitle": "登陆以探索更广大的世界。",
"accountLogout": "退出登录",
"accountLogoutSubtitle": "注销当前账户的登陆状态。",
"accountLogoutConfirmTitle": "您确定要退出登录吗?",
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
"accountPublishers": "你的发布者",
"accountPublishersSubtitle": "管理你的公共形象。"
} }

View File

@ -4,12 +4,11 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
class SnAttachmentProvider { class SnAttachmentProvider {
late final SnNetworkProvider sn; late final SnNetworkProvider _sn;
final Map<String, SnAttachment> _cache = {}; final Map<String, SnAttachment> _cache = {};
SnAttachmentProvider(BuildContext context) { SnAttachmentProvider(BuildContext context) {
sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
} }
Future<SnAttachment> getOne(String rid, {noCache = false}) async { Future<SnAttachment> getOne(String rid, {noCache = false}) async {
@ -17,7 +16,7 @@ class SnAttachmentProvider {
return _cache[rid]!; return _cache[rid]!;
} }
final resp = await sn.client.get('/cgi/uc/attachments/$rid/meta'); final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
final out = SnAttachment.fromJson(resp.data); final out = SnAttachment.fromJson(resp.data);
_cache[rid] = out; _cache[rid] = out;
@ -33,7 +32,7 @@ class SnAttachmentProvider {
return rids.map((rid) => _cache[rid]!).toList(); return rids.map((rid) => _cache[rid]!).toList();
} }
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { final resp = await _sn.client.get('/cgi/uc/attachments', queryParameters: {
'take': pendingFetch.length, 'take': pendingFetch.length,
'id': pendingFetch.join(','), 'id': pendingFetch.join(','),
}); });

View File

@ -67,7 +67,7 @@ class SnNetworkProvider {
final b64 = utf8.fuse(base64Url); final b64 = utf8.fuse(base64Url);
final payload = b64.decode(rawPayload); final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp']; final exp = jsonDecode(payload)['exp'];
if (exp >= DateTime.now().millisecondsSinceEpoch) { if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('Access token need refresh, doing it at ${DateTime.now()}'); log('Access token need refresh, doing it at ${DateTime.now()}');
atk = await refreshToken(); atk = await refreshToken();
} }
@ -78,6 +78,8 @@ class SnNetworkProvider {
log('Access token refresh failed...'); log('Access token refresh failed...');
} }
} }
} catch (err) {
log('Failed to authenticate user: $err');
} finally { } finally {
handler.next(options); handler.next(options);
} }
@ -114,7 +116,12 @@ class SnNetworkProvider {
final rtk = await _storage.read(key: kRtkStoreKey); final rtk = await _storage.read(key: kRtkStoreKey);
if (rtk == null) return null; if (rtk == null) return null;
final resp = await client.post('/cgi/id/auth/token', data: { final dio = Dio();
dio.options.baseUrl = kUseLocalNetwork
? 'http://localhost:8001'
: 'https://api.sn.solsynth.dev';
final resp = await dio.post('/cgi/id/auth/token', data: {
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
'refresh_token': rtk, 'refresh_token': rtk,
}); });

View File

@ -10,12 +10,11 @@ class UserProvider extends ChangeNotifier {
bool isAuthorized = false; bool isAuthorized = false;
SnAccount? user; SnAccount? user;
late final SnNetworkProvider sn; late final SnNetworkProvider _sn;
late final FlutterSecureStorage _storage = FlutterSecureStorage(); late final FlutterSecureStorage _storage = FlutterSecureStorage();
UserProvider(BuildContext context) { UserProvider(BuildContext context) {
sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_storage.read(key: kAtkStoreKey).then((value) { _storage.read(key: kAtkStoreKey).then((value) {
isAuthorized = value != null; isAuthorized = value != null;
@ -31,7 +30,7 @@ class UserProvider extends ChangeNotifier {
Future<SnAccount?> refreshUser() async { Future<SnAccount?> refreshUser() async {
if (!isAuthorized) return null; if (!isAuthorized) return null;
final resp = await sn.client.get('/cgi/id/users/me'); final resp = await _sn.client.get('/cgi/id/users/me');
final out = SnAccount.fromJson(resp.data); final out = SnAccount.fromJson(resp.data);
user = out; user = out;
@ -39,4 +38,11 @@ class UserProvider extends ChangeNotifier {
return out; return out;
} }
void logoutUser() async {
_sn.clearTokenPair();
isAuthorized = false;
user = null;
notifyListeners();
}
} }

View File

@ -1,44 +1,153 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountScreen extends StatefulWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@override
State<AccountScreen> createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),
), ),
body: ListView( body: SingleChildScrollView(
children: [ child: ua.isAuthorized
ListTile( ? _AuthorizedAccountScreen()
title: Text('screenAuthLogin').tr(), : _UnauthorizedAccountScreen(),
subtitle: Text('screenAuthLoginSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Icons.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('authLogin');
},
),
ListTile(
title: Text('screenAuthRegister').tr(),
subtitle: Text('screenAuthRegisterSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Icons.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('authRegister');
},
)
],
), ),
); );
} }
} }
class _AuthorizedAccountScreen extends StatelessWidget {
const _AuthorizedAccountScreen({super.key});
@override
Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
return Column(
children: [
Card(
child: Builder(builder: (context) {
if (ua.user == null) {
return SizedBox(
width: double.infinity,
height: 120,
child: CircularProgressIndicator().center(),
);
}
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountImage(content: ua.user!.avatar, radius: 28),
const Gap(8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(ua.user!.nick)
.textStyle(Theme.of(context).textTheme.titleLarge!),
const Gap(4),
Text('@${ua.user!.name}')
.textStyle(Theme.of(context).textTheme.bodySmall!),
],
),
Text(ua.user!.description)
.textStyle(Theme.of(context).textTheme.bodyMedium!),
],
),
);
}).padding(all: 20),
).padding(horizontal: 8, top: 16, bottom: 4),
ListTile(
title: Text('accountPublishers').tr(),
subtitle: Text('accountPublishersSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.face),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.logout),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context
.showConfirmDialog(
'accountLogoutConfirmTitle'.tr(),
'accountLogoutConfirm'.tr(),
)
.then((value) {
if (value) ua.logoutUser();
});
},
),
],
);
}
}
class _UnauthorizedAccountScreen extends StatelessWidget {
const _UnauthorizedAccountScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
Card(
child: SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.waving_hand, size: 32),
const Gap(8),
Text('accountIntroTitle')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroSubtitle').tr(),
],
).padding(all: 20),
),
).padding(horizontal: 8, top: 16, bottom: 4),
ListTile(
title: Text('screenAuthLogin').tr(),
subtitle: Text('screenAuthLoginSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.login),
trailing: const Icon(Icons.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('authLogin');
},
),
ListTile(
title: Text('screenAuthRegister').tr(),
subtitle: Text('screenAuthRegisterSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.person_add),
trailing: const Icon(Icons.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('authRegister');
},
),
],
);
}
}