mirror of
https://github.com/hanskokx/arcane_implementations.git
synced 2026-06-09 15:16:06 +02:00
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user