Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2024-09-19 13:34:51 +02:00
commit c7ed42b1c7
16 changed files with 1390 additions and 0 deletions
@@ -0,0 +1,94 @@
import "dart:developer";
import "package:arcane_framework/arcane_framework.dart";
import "package:arcane_helper_utils/arcane_helper_utils.dart";
import "package:dio/dio.dart";
class AuthorizationInterceptor extends Interceptor {
AuthorizationInterceptor();
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (options.path == EnvVar.accessTokenUrl.value) {
return super.onRequest(options, handler);
}
final bool isSignedIn = Arcane.auth.isSignedIn.value;
String? accessToken;
if (isSignedIn) accessToken = await Arcane.auth.accessToken;
// Gather IDs for each request so we can track each HTTP request by session,
// install, and per-request. These IDs can be used to correlate requests
// between the front-end and backend in the logs.
final String requestId = IdService.I.newId;
final String? installId = IdService.I.installId;
final String? sessionId = IdService.I.sessionId.value;
// Logs the auth token to the debug console. Useful for debugging requests
// using third-party tools where you need to authenticate. Only works if the
// auth token isn't null and [Feature.debugPrintAuthToken] is enabled.
if (accessToken != null && Feature.debugPrintAuthToken.enabled) {
log("Token expires at ${accessToken.jwtExpiryTime()!.toIso8601String()}\n\n$accessToken");
}
// Assembles a Map of metadata that we're going to add to each HTTP request
// so we can log out all the information in the debug console. We also
// store all of the IDs we've collected into a Map for future use.
final Map<String, String> metadata = {
"request_id": requestId,
if (installId.isNotNullOrEmpty) "install_id": "$installId",
if (sessionId.isNotNullOrEmpty) "session_id": "$sessionId",
};
// Attempts to map the "query" parameter in a GraphQL request to a String
// so that we can log it out in the debug console.
try {
final Map<String, dynamic> query = options.data as Map<String, dynamic>;
for (final MapEntry<String, dynamic> entry in query.entries) {
if (entry.key == "query") {
final String queryString = entry.value.toString();
final int queryStringOriginalLength = queryString.length;
const int trimQueryStringAtLength = 64;
final int trimToLength =
queryStringOriginalLength > trimQueryStringAtLength
? trimQueryStringAtLength
: queryStringOriginalLength;
final String finalString =
"${queryString.substring(0, trimToLength)}${trimToLength < queryStringOriginalLength ? "[...]" : ""}";
metadata.addAll({entry.key: finalString});
}
}
} catch (e) {
log(e.toString());
}
// Removes the extra query string from the headers so we're not sending it
// to the server along with our request.
metadata.removeWhere((key, value) => key == "query");
// Tries to add the authentication token to the header metadata that we're
// going to add to the request.
if (accessToken.isNotNullOrEmpty) {
metadata.addAll({"Authorization": "Bearer $accessToken"});
}
// Adds all the collected metadata (auth token, request ID, etc.) to the
// request headers.
options.headers.addAll(metadata);
// Logs the HTTP request so we can see the request ID and truncated query
// string (if available).
Arcane.log(
"Making HTTP Request ${options.path}",
level: Level.info,
metadata: metadata,
);
return super.onRequest(options, handler);
}
}
+92
View File
@@ -0,0 +1,92 @@
import "package:arcane_framework/arcane_framework.dart";
import "package:dio/dio.dart";
import "package:flutter/foundation.dart";
import "package:gql_dio_link/gql_dio_link.dart";
import "package:graphql/client.dart";
class AppHttpClient {
static bool _isMocked = false;
static final AppHttpClient _service = AppHttpClient._internal();
factory AppHttpClient() {
return _service;
}
AppHttpClient._internal();
static late GraphQLClient _graphQlClient;
static GraphQLClient get graphQlClient => _graphQlClient;
static late GraphQLClient _webSocketClient;
static GraphQLClient get webSocketClient => _webSocketClient;
@visibleForTesting
static void setMocked() => _isMocked = true;
static Future<void> init(Dio dio, {String? url}) async {
if (_isMocked) return;
late final String? authToken;
try {
authToken = await Arcane.auth.accessToken;
} catch (e) {
return;
}
_webSocketClient = GraphQLClient(
cache: GraphQLCache(),
queryRequestTimeout: const Duration(seconds: 40),
defaultPolicies: DefaultPolicies(
query: Policies(
fetch: FetchPolicy.networkOnly,
),
),
link: WebSocketLink(
"wss://$url",
config: SocketClientConfig(
autoReconnect: true,
initialPayload: {
"Authorization": "Bearer $authToken",
},
),
subProtocol: GraphQLProtocol.graphqlTransportWs,
),
);
_graphQlClient = GraphQLClient(
cache: GraphQLCache(),
queryRequestTimeout: const Duration(seconds: 40),
defaultPolicies: DefaultPolicies(
query: Policies(
fetch: FetchPolicy.networkOnly,
),
),
link: DioLink(
"https://$url",
client: dio,
),
);
}
}
extension QueryResultExtension on QueryResult {
int get errorStatusCode {
if (exception?.linkException != null &&
exception!.linkException is HttpLinkServerException) {
final HttpLinkServerException httpLinkException =
exception!.linkException as HttpLinkServerException;
return httpLinkException.response.statusCode;
}
return -1;
}
bool get failed {
return !success;
}
bool get success {
return !hasException && (data ?? const <dynamic, dynamic>{}).isNotEmpty;
}
}
+63
View File
@@ -0,0 +1,63 @@
import "package:arcane_framework/arcane_framework.dart";
import "package:dio/dio.dart";
import "package:dio_smart_retry/dio_smart_retry.dart";
import "package:get_it/get_it.dart";
import "package:native_dio_adapter/native_dio_adapter.dart";
import "package:pretty_dio_logger/pretty_dio_logger.dart";
import "authorization_interceptor.dart";
abstract class DioHelper {
DioHelper._();
static Dio createDioInstance(GetIt container) {
final Dio dio = Dio(
BaseOptions(
baseUrl: "https://${EnvVar.graphQlUrl.value}",
connectTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 40),
followRedirects: false,
),
);
dio.interceptors
..add(container.get<AuthorizationInterceptor>())
..add(dioLogger)
..add(smartRetry(dio));
dio.httpClientAdapter = NativeAdapter();
return dio;
}
static Interceptor smartRetry(Dio dio) => RetryInterceptor(
dio: dio,
logPrint: Arcane.log,
retries: 3,
retryDelays: const [
Duration(seconds: 1),
Duration(seconds: 2),
Duration(seconds: 3),
],
);
static PrettyDioLogger get dioLogger => PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
compact: true,
maxWidth: 90,
enabled: Feature.httpLogging.enabled,
filter: (options, args) {
if (options.path.contains("/graphql")) {
return true;
}
// don't print responses with unit8 list data
return !args.isResponse || !args.hasUint8ListData;
},
);
}