From 3089e1f8d29436a5862098fc46f5861330751476 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 2 May 2024 12:16:01 +0800 Subject: [PATCH] :zap: Rewrite http client --- lib/providers/auth.dart | 61 ++++++++++-------------- lib/providers/chat.dart | 4 +- lib/providers/notify.dart | 4 +- lib/screens/chat/index.dart | 37 ++++++++------- lib/screens/posts/comment_editor.dart | 4 +- lib/screens/posts/moment_editor.dart | 4 +- lib/utils/http.dart | 67 +++++++++++++++++++++++++++ lib/widgets/chat/message_editor.dart | 4 +- 8 files changed, 121 insertions(+), 64 deletions(-) create mode 100644 lib/utils/http.dart diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 1d87ff4..4830767 100755 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -3,13 +3,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:oauth2/oauth2.dart' as oauth2; +import 'package:solian/utils/http.dart'; import 'package:solian/utils/service_url.dart'; class AuthProvider extends ChangeNotifier { AuthProvider(); - final deviceEndpoint = - getRequestUri('passport', '/api/notifications/subscribe'); + final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe'); final tokenEndpoint = getRequestUri('passport', '/api/auth/token'); final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); final redirectUrl = Uri.parse('solian://auth'); @@ -21,19 +21,17 @@ class AuthProvider extends ChangeNotifier { static const storageKey = 'identity'; static const profileKey = 'profiles'; - /// Before use this variable to make request - /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD** - oauth2.Client? client; - - DateTime? lastRefreshedAt; + HttpClient? client; Future loadClient() async { if (await storage.containsKey(key: storageKey)) { try { - final credentials = - oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); - client = oauth2.Client(credentials, - identifier: clientId, secret: clientSecret); + final credentials = oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); + client = HttpClient( + defaultToken: credentials.accessToken, + defaultRefreshToken: credentials.refreshToken, + onTokenRefreshed: setToken, + ); await fetchProfiles(); return true; } catch (e) { @@ -45,13 +43,12 @@ class AuthProvider extends ChangeNotifier { } } - Future createClient( - BuildContext context, String username, String password) async { + Future createClient(BuildContext context, String username, String password) async { if (await loadClient()) { return client!; } - return await oauth2.resourceOwnerPasswordGrant( + final credentials = (await oauth2.resourceOwnerPasswordGrant( tokenEndpoint, username, password, @@ -59,6 +56,15 @@ class AuthProvider extends ChangeNotifier { secret: clientSecret, scopes: ['openid'], basicAuth: false, + )) + .credentials; + + setToken(credentials.accessToken, credentials.refreshToken!); + + return HttpClient( + defaultToken: credentials.accessToken, + defaultRefreshToken: credentials.refreshToken, + onTokenRefreshed: setToken, ); } @@ -70,21 +76,16 @@ class AuthProvider extends ChangeNotifier { notifyListeners(); } - Future refreshToken() async { + Future setToken(String atk, String rtk) async { if (client != null) { - final credentials = await client!.credentials.refresh( - identifier: clientId, secret: clientSecret, basicAuth: false); - client = oauth2.Client(credentials, - identifier: clientId, secret: clientSecret); + final credentials = oauth2.Credentials(atk, refreshToken: rtk, idToken: atk, scopes: ['openid']); storage.write(key: storageKey, value: credentials.toJson()); } notifyListeners(); } - Future signin( - BuildContext context, String username, String password) async { + Future signin(BuildContext context, String username, String password) async { client = await createClient(context, username, password); - storage.write(key: storageKey, value: client!.credentials.toJson()); await fetchProfiles(); } @@ -96,21 +97,7 @@ class AuthProvider extends ChangeNotifier { Future isAuthorized() async { const storage = FlutterSecureStorage(); - if (await storage.containsKey(key: storageKey)) { - if (client == null) { - await loadClient(); - } - if (lastRefreshedAt == null || - DateTime.now() - .subtract(const Duration(minutes: 3)) - .isAfter(lastRefreshedAt!)) { - await refreshToken(); - lastRefreshedAt = DateTime.now(); - } - return true; - } else { - return false; - } + return await storage.containsKey(key: storageKey); } Future getProfiles() async { diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index e74e7a9..f9ce2d4 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -28,14 +28,14 @@ class ChatProvider extends ChangeNotifier { if (auth.client == null) await auth.loadClient(); if (!await auth.isAuthorized()) return null; - await auth.refreshToken(); + await auth.client!.refreshToken(auth.client!.currentRefreshToken!); var ori = getRequestUri('messaging', '/api/ws'); var uri = Uri( scheme: ori.scheme.replaceFirst('http', 'ws'), host: ori.host, path: ori.path, - queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, + queryParameters: {'tk': Uri.encodeComponent(auth.client!.currentToken!)}, ); final channel = WebSocketChannel.connect(uri); diff --git a/lib/providers/notify.dart b/lib/providers/notify.dart index a16292d..3fea6a0 100644 --- a/lib/providers/notify.dart +++ b/lib/providers/notify.dart @@ -72,7 +72,7 @@ class NotifyProvider extends ChangeNotifier { if (auth.client == null) await auth.loadClient(); if (!await auth.isAuthorized()) return null; - await auth.refreshToken(); + await auth.client!.refreshToken(auth.client!.currentRefreshToken!); var ori = getRequestUri('passport', '/api/notifications/listen'); var uri = Uri( @@ -80,7 +80,7 @@ class NotifyProvider extends ChangeNotifier { host: ori.host, path: ori.path, queryParameters: { - 'tk': Uri.encodeComponent(auth.client!.credentials.accessToken) + 'tk': Uri.encodeComponent(auth.client!.currentToken!) }, ); diff --git a/lib/screens/chat/index.dart b/lib/screens/chat/index.dart index 3a565a0..79e270a 100644 --- a/lib/screens/chat/index.dart +++ b/lib/screens/chat/index.dart @@ -73,28 +73,15 @@ class _ChatIndexScreenState extends State { ), ], ) - : ChatIndexScreenWidget( - onSelect: (item) async { - final result = await router.pushNamed( - 'chat.channel', - pathParameters: { - 'channel': item.alias, - }, - ); - switch (result) { - case 'refresh': - // fetchChannels(); - } - }, - ), + : const ChatIndexScreenWidget(), ); } } class ChatIndexScreenWidget extends StatefulWidget { - final Function(Channel item) onSelect; + final Function(Channel item)? onSelect; - const ChatIndexScreenWidget({super.key, required this.onSelect}); + const ChatIndexScreenWidget({super.key, this.onSelect}); @override State createState() => _ChatIndexScreenWidgetState(); @@ -176,7 +163,23 @@ class _ChatIndexScreenWidgetState extends State { ), title: Text(element.name), subtitle: Text(element.description), - onTap: () => widget.onSelect(element), + onTap: () async { + if (widget.onSelect != null) { + widget.onSelect!(element); + return; + } + + final result = await router.pushNamed( + 'chat.channel', + pathParameters: { + 'channel': element.alias, + }, + ); + switch (result) { + case 'refresh': + fetchChannels(); + } + }, ); }, ), diff --git a/lib/screens/posts/comment_editor.dart b/lib/screens/posts/comment_editor.dart index 9133149..ca74d96 100644 --- a/lib/screens/posts/comment_editor.dart +++ b/lib/screens/posts/comment_editor.dart @@ -175,9 +175,9 @@ class _CommentEditorScreenState extends State { ), widget.editing != null ? editingBanner : Container(), Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - top: BorderSide(width: 0.3, color: Color(0xffdedede)), + top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor), ), ), child: Row( diff --git a/lib/screens/posts/moment_editor.dart b/lib/screens/posts/moment_editor.dart index b29820c..8549d22 100644 --- a/lib/screens/posts/moment_editor.dart +++ b/lib/screens/posts/moment_editor.dart @@ -165,9 +165,9 @@ class _MomentEditorScreenState extends State { ), widget.editing != null ? editingBanner : Container(), Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - top: BorderSide(width: 0.3, color: Color(0xffdedede)), + top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor), ), ), child: Row( diff --git a/lib/utils/http.dart b/lib/utils/http.dart new file mode 100644 index 0000000..531e544 --- /dev/null +++ b/lib/utils/http.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:solian/utils/service_url.dart'; + +class HttpClient extends http.BaseClient { + final bool isUnauthorizedRetry; + final Future Function()? onUnauthorizedRetry; + final Function(String atk, String rtk)? onTokenRefreshed; + + final _client = http.Client(); + + HttpClient({ + this.isUnauthorizedRetry = true, + this.onUnauthorizedRetry, + this.onTokenRefreshed, + String? defaultToken, + String? defaultRefreshToken, + }) { + currentToken = defaultToken; + currentRefreshToken = defaultRefreshToken; + } + + String? currentToken; + String? currentRefreshToken; + + Future refreshToken(String token) async { + final res = await _client.post( + getRequestUri('passport', '/api/auth/token'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'refresh_token': token, 'grant_type': 'refresh_token'}), + ); + if (res.statusCode != 200) { + var message = utf8.decode(res.bodyBytes); + throw Exception('An error occurred when trying refresh token: $message'); + } + final result = jsonDecode(utf8.decode(res.bodyBytes)); + currentToken = result['access_token']; + currentRefreshToken = result['refresh_token']; + if (onTokenRefreshed != null) { + onTokenRefreshed!(currentToken!, currentRefreshToken!); + } + return currentToken!; + } + + @override + Future send(http.BaseRequest request) async { + request.headers['Authorization'] = 'Bearer $currentToken'; + + final res = await _client.send(request); + if (res.statusCode == 401 && isUnauthorizedRetry) { + if (onUnauthorizedRetry != null) { + currentToken = await onUnauthorizedRetry!(); + } else if (currentRefreshToken != null) { + currentToken = await refreshToken(currentRefreshToken!); + } else { + final result = await http.Response.fromStream(res); + throw Exception(utf8.decode(result.bodyBytes)); + } + + request.headers['Authorization'] = 'Bearer $currentToken'; + return await _client.send(request); + } + + return res; + } +} diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index 42b0bf0..c91fb46 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -140,9 +140,9 @@ class _ChatMessageEditorState extends State { Container( height: 56, padding: const EdgeInsets.only(top: 4, bottom: 4, right: 8), - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - top: BorderSide(width: 0.3, color: Color(0xffdedede)), + top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor), ), ), child: Row(