From ea4a30ca00c4184b4ef05baf93420c1c53122402 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 23 Jun 2022 11:48:32 +0200 Subject: [PATCH] feat(http): [WIP] work on middleware feature --- .../example/http_client_fastapi_example.dart | 386 ++++++++++++++++++ .../example/http_client_test.dart | 41 ++ .../interfaces/authenticated_client.dart | 87 ++++ .../interfaces/authentication_client.dart | 197 +++++++++ .../header_authentication_client.dart | 1 + .../interfaces/oauth2_client.dart | 7 +- .../authentication/refresh_token_client.dart | 86 +++- .../lib/src/implemented_base_client.dart | 31 ++ .../lib/src/implemented_client.dart | 86 ++++ .../lib/src/mixins/body_transformer.dart | 83 ++++ .../lib/src/mixins/headers_transformer.dart | 131 ++++++ .../lib/src/mixins/oauth2_transformer.dart | 33 ++ .../lib/src/mixins/request_transformer.dart | 27 ++ .../lib/src/rest_client.dart | 166 +++++++- 14 files changed, 1349 insertions(+), 13 deletions(-) create mode 100644 packages/wyatt_http_client/example/http_client_fastapi_example.dart create mode 100644 packages/wyatt_http_client/example/http_client_test.dart create mode 100644 packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart create mode 100644 packages/wyatt_http_client/lib/src/implemented_base_client.dart create mode 100644 packages/wyatt_http_client/lib/src/implemented_client.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/body_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/request_transformer.dart diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart new file mode 100644 index 00000000..21c13b42 --- /dev/null +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -0,0 +1,386 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart'; +import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; + +enum EmailVerificationAction { + signUp, + resetPassword, + changeEmail; + + String toSnakeCase() { + return name.splitMapJoin( + RegExp('[A-Z]'), + onMatch: (m) => '_${m[0]?.toLowerCase()}', + onNonMatch: (n) => n, + ); + } + + factory EmailVerificationAction.fromString(String str) { + return EmailVerificationAction.values.firstWhere( + (EmailVerificationAction element) => element.toSnakeCase() == str, + ); + } +} + +class VerifyCode { + final String email; + final String verificationCode; + final EmailVerificationAction action; + VerifyCode({ + required this.email, + required this.verificationCode, + required this.action, + }); + + VerifyCode copyWith({ + String? email, + String? verificationCode, + EmailVerificationAction? action, + }) { + return VerifyCode( + email: email ?? this.email, + verificationCode: verificationCode ?? this.verificationCode, + action: action ?? this.action, + ); + } + + Map toMap() { + return { + 'email': email, + 'verification_code': verificationCode, + 'action': action.toSnakeCase(), + }; + } + + factory VerifyCode.fromMap(Map map) { + return VerifyCode( + email: map['email'] as String, + verificationCode: map['verification_code'] as String, + action: EmailVerificationAction.fromString(map['action'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory VerifyCode.fromJson(String source) => + VerifyCode.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'VerifyCode(email: $email, verificationCode: $verificationCode, action: $action)'; +} + +class Account { + final String email; + final String? sessionId; + Account({ + required this.email, + this.sessionId, + }); + + Account copyWith({ + String? email, + String? sessionId, + }) { + return Account( + email: email ?? this.email, + sessionId: sessionId ?? this.sessionId, + ); + } + + Map toMap() { + return { + 'email': email, + 'session_id': sessionId, + }; + } + + factory Account.fromMap(Map map) { + return Account( + email: map['email'] as String, + sessionId: map['session_id'] != null ? map['session_id'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory Account.fromJson(String source) => + Account.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Account(email: $email, sessionId: $sessionId)'; +} + +class SignUp { + final String sessionId; + final String password; + SignUp({ + required this.sessionId, + required this.password, + }); + + SignUp copyWith({ + String? sessionId, + String? password, + }) { + return SignUp( + sessionId: sessionId ?? this.sessionId, + password: password ?? this.password, + ); + } + + Map toMap() { + return { + 'session_id': sessionId, + 'password': password, + }; + } + + factory SignUp.fromMap(Map map) { + return SignUp( + sessionId: map['session_id'] as String, + password: map['password'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory SignUp.fromJson(String source) => + SignUp.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SignUp(sessionId: $sessionId, password: $password)'; +} + +class TokenSuccess { + final String accessToken; + final String refreshToken; + final Account account; + TokenSuccess({ + required this.accessToken, + required this.refreshToken, + required this.account, + }); + + TokenSuccess copyWith({ + String? accessToken, + String? refreshToken, + Account? account, + }) { + return TokenSuccess( + accessToken: accessToken ?? this.accessToken, + refreshToken: refreshToken ?? this.refreshToken, + account: account ?? this.account, + ); + } + + Map toMap() { + return { + 'access_token': accessToken, + 'refresh_token': refreshToken, + 'account': account.toMap(), + }; + } + + factory TokenSuccess.fromMap(Map map) { + return TokenSuccess( + accessToken: map['access_token'] as String, + refreshToken: map['refresh_token'] as String, + account: Account.fromMap(map['account'] as Map), + ); + } + + String toJson() => json.encode(toMap()); + + factory TokenSuccess.fromJson(String source) => + TokenSuccess.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'TokenSuccess(accessToken: $accessToken, refreshToken: $refreshToken, account: $account)'; +} + +class Login { + final String email; + final String password; + Login({ + required this.email, + required this.password, + }); + + Login copyWith({ + String? email, + String? password, + }) { + return Login( + email: email ?? this.email, + password: password ?? this.password, + ); + } + + Map toMap() { + return { + 'email': email, + 'password': password, + }; + } + + factory Login.fromMap(Map map) { + return Login( + email: map['email'] as String, + password: map['password'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory Login.fromJson(String source) => + Login.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Login(email: $email, password: $password)'; +} + +class FastAPI { + final String baseUrl; + final RefreshTokenClient client; + final int apiVersion; + + FastAPI({ + this.baseUrl = 'localhost:80', + RefreshTokenClient? client, + this.apiVersion = 1, + }) : client = client ?? + RefreshTokenClient( + authorizationEndpoint: '', + tokenEndpoint: '', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + inner: RestClient( + protocol: Protocols.http, + authority: baseUrl, + ), + ); + + String get apiPath => '/api/v$apiVersion'; + + Future sendSignUpCode(String email) async { + final r = await client.post( + Uri.parse('$apiPath/auth/send-sign-up-code'), + body: { + 'email': email, + }, + ); + if (r.statusCode != 201) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } + } + + Future verifyCode(VerifyCode verifyCode) async { + final r = await client.post( + Uri.parse('$apiPath/auth/verify-code'), + body: verifyCode.toMap(), + ); + if (r.statusCode != 202) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return Account.fromMap( + (jsonDecode(r.body) as Map)['account'] + as Map, + ); + } + } + + Future signUp(SignUp signUp) async { + final r = await client.post( + Uri.parse('$apiPath/auth/sign-up'), + body: signUp.toMap(), + ); + if (r.statusCode != 201) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return Account.fromJson(r.body); + } + } + + Future signInWithPassword(Login login) async { + final r = await client.authorize(login.toMap()); + return TokenSuccess.fromJson(r.body); + } + + Future refresh() async { + final r = await client.refresh(); + return TokenSuccess.fromJson(r?.body ?? ''); + } + + Future> getAccountList() async { + final r = await client.get( + Uri.parse('$apiPath/account'), + ); + if (r.statusCode != 200) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + final list = (jsonDecode(r.body) as Map)['founds'] + as List>; + final result = []; + for (final element in list) { + result.add(Account.fromMap(element)); + } + return result; + } + } +} + +void main(List args) async { + final api = FastAPI( + client: RefreshTokenClient( + authorizationEndpoint: '/api/v1/auth/sign-in-with-password', + tokenEndpoint: '/api/v1/auth/refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + inner: RestClient( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ), + ); + + // await api.sendSignUpCode('git@pcl.ovh'); + // final verifiedAccount = await api.verifyCode( + // VerifyCode( + // email: 'git@pcl.ovh', + // verificationCode: '000000000', + // action: EmailVerificationAction.signUp, + // ), + // ); + // print(verifiedAccount); + // final registeredAccount = await api.signUp( + // SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'), + // ); + // print(registeredAccount); + final signedInAccount = await api.signInWithPassword( + Login(email: 'git@pcl.ovh', password: 'password'), + ); + // print(signedInAccount); + final accountList = await api.getAccountList(); + print(accountList); +} diff --git a/packages/wyatt_http_client/example/http_client_test.dart b/packages/wyatt_http_client/example/http_client_test.dart new file mode 100644 index 00000000..36a66c15 --- /dev/null +++ b/packages/wyatt_http_client/example/http_client_test.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +Future main(List args) async { + // final client = Oauth2Client( + // accessToken: 'test-token', + // inner: RestClient(protocol: Protocols.http, authority: 'localhost:80'), + // ); + // final client = RestClient( + // protocol: Protocols.http, + // authority: 'localhost:80', + // inner: Oauth2Client( + // authorizationEndpoint: '/api/v1/account/test', + // tokenEndpoint: '/api/v1/account/test', + // accessToken: 'test-token', + // refreshToken: 'refresh-token', + // ), + // ); + // final client = RestClient(protocol: Protocols.http, authority: 'localhost:80'); + // final client =AwesomeRestClient(protocol: Protocols.http, authority: 'localhost:80'); + // var r = await client.post( + // Uri.parse('/api/v1/account/test'), + // body: { + // 'email': 'test@test.fr', + // }, + // ); +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart new file mode 100644 index 00000000..0c8cc7f1 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart @@ -0,0 +1,87 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +abstract class AuthenticatedClient implements ImplementedBaseClient { + final Client _inner; + + AuthenticatedClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart index 9e956216..94c844d4 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart @@ -14,8 +14,161 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; +import 'package:wyatt_http_client/src/utils/utils.dart'; + +class AwesomeClient extends BaseClient { + final Client _inner; + + AwesomeClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + Future send(BaseRequest request) { + return _inner.send(request); + } +} + +class AwesomeRestClient extends AwesomeClient + with HeadersTransformer, BodyTransformer { + final Protocols protocol; + final String? authority; + + AwesomeRestClient({ + this.protocol = Protocols.https, + this.authority = '', + super.inner, + }); + + @override + Object? bodyMutator(Object? body) { + print('bodyMutator: Json encoding'); + return jsonEncode(body); + } + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print('headerMutator: Json encoding'); + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } + + @override + BaseRequest requestMutator(BaseRequest request) { + print('requestMutator: scheme + authority'); + final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); + return Utils.copyRequestWith(request, url: uri); + } +} + +class AwesomeUrlClient extends AuthenticatedClient { + AwesomeUrlClient(super.inner); +} + +class AwesomeOauth2Client extends AuthenticatedClient with HeadersTransformer { + AwesomeOauth2Client(super.inner); + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print('headersMutator: Token manager'); + final mutation = { + 'authorization': 'Bearer TOKEN', + }; + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } +} + +abstract class AuthenticatedClient implements Client { + final Client _inner; + + Client get inner => _inner; + + AuthenticatedClient(BaseClient? inner) : _inner = inner ?? RestClient(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} abstract class AuthenticationClient extends BaseClient { final BaseClient _inner; @@ -24,6 +177,50 @@ abstract class AuthenticationClient extends BaseClient { AuthenticationClient(BaseClient? inner) : _inner = inner ?? RestClient(); + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + @override Future send(BaseRequest request) { return _inner.send(request); diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart index c206748f..ef8aae8d 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart @@ -31,6 +31,7 @@ abstract class HeaderAuthenticationClient extends AuthenticationClient { final newHeader = modifyHeader(Map.from(request.headers), request); request.headers.clear(); request.headers.addAll(newHeader); + print(newHeader); return super.send(request); } } diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart index 1da70e99..5704788c 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:http/http.dart'; import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; typedef TokenParser = String Function(Map); @@ -21,14 +22,14 @@ typedef TokenParser = String Function(Map); abstract class Oauth2Client extends HeaderAuthenticationClient { Oauth2Client(super.inner); - Future refresh() { + Future refresh() { return Future.value(); } - Future authorize( + Future authorize( Map body, { Map? headers, }) { - return Future.value(); + return Future.value(); } } diff --git a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart index 67d6a1d0..01201966 100644 --- a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart @@ -23,6 +23,78 @@ import 'package:wyatt_http_client/src/utils/header_keys.dart'; import 'package:wyatt_http_client/src/utils/http_status.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; +// class Oauth2Client extends ImplementedBaseClient with RequestTransformer { +// final String authorizationEndpoint; +// final String tokenEndpoint; +// String? accessToken; +// String? refreshToken; +// String? tokenToUse; + +// Oauth2Client({ +// required this.authorizationEndpoint, +// required this.tokenEndpoint, +// this.accessToken, +// this.refreshToken, +// super.inner, +// }) : tokenToUse = accessToken; + +// @override +// BaseRequest requestMutator(BaseRequest request) { +// print('Oauth2Client::requestMutator -> add authorization: $accessToken'); +// final headers = request.headers; +// final mutation = { +// 'Authorization': 'Bearer $tokenToUse', +// }; +// if (tokenToUse?.isNotEmpty ?? false) { +// headers.addAll(mutation); +// return Utils.copyRequestWith(request, headers: headers); +// } +// return request; +// } + +// Future refresh() async { +// if (refreshToken?.isNotEmpty ?? false) { +// tokenToUse = refreshToken; + +// final response = await get( +// Uri.parse(tokenEndpoint), +// ); + +// if (response.statusCode == HttpStatus.ok) { +// // final body = json.decode(response.body) as Map; +// // accessToken = accessTokenParser(body); +// print('Oauth2Client::refresh -> ok'); +// } +// return response; +// } +// return null; +// } + +// @override +// Map? headersMutator( +// Object? body, +// Map? headers, +// ) { +// print( +// 'Oauth2Client::headersMutator -> add authorization: $accessToken', +// ); +// final mutation = { +// 'Authorization': 'Bearer $accessToken', +// }; +// if (accessToken.isNotEmpty) { +// if (headers != null) { +// headers.addAll(mutation); +// return headers; +// } else { +// return mutation; +// } +// } else { +// return headers; +// } +// } + +// } + class RefreshTokenClient extends Oauth2Client { final String authorizationEndpoint; final String tokenEndpoint; @@ -50,6 +122,8 @@ class RefreshTokenClient extends Oauth2Client { Map header, [ BaseRequest? request, ]) { + print('accessToken $accessToken'); + print('request $request'); if (accessToken != null && request != null) { header[authenticationHeader] = '$authenticationMethod $accessToken'; return header; @@ -58,13 +132,13 @@ class RefreshTokenClient extends Oauth2Client { } @override - Future authorize( + Future authorize( Map body, { Map? headers, }) async { final response = await inner.post( Uri.parse(authorizationEndpoint), - body: jsonEncode(body), + body: body, headers: headers, ); @@ -80,10 +154,11 @@ class RefreshTokenClient extends Oauth2Client { this.refreshToken = refreshToken; } } + return response; } @override - Future refresh() async { + Future refresh() async { if (refreshToken != null) { final Map header = { authenticationHeader: '$authenticationHeader $refreshToken', @@ -98,11 +173,16 @@ class RefreshTokenClient extends Oauth2Client { final body = json.decode(response.body) as Map; accessToken = accessTokenParser(body); } + return response; } + return null; } @override Future send(BaseRequest request) async { + final newHeader = modifyHeader(Map.from(request.headers), request); + request.headers.clear(); + request.headers.addAll(newHeader); final response = await super.send(request); if (response.statusCode == HttpStatus.unauthorized) { diff --git a/packages/wyatt_http_client/lib/src/implemented_base_client.dart b/packages/wyatt_http_client/lib/src/implemented_base_client.dart new file mode 100644 index 00000000..e42ed84f --- /dev/null +++ b/packages/wyatt_http_client/lib/src/implemented_base_client.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; + +class ImplementedBaseClient extends BaseClient { + final Client inner; + + ImplementedBaseClient({ + Client? inner, + }) : inner = inner ?? Client(); + + @override + Future send(BaseRequest request) { + return inner.send(request); + } + +} diff --git a/packages/wyatt_http_client/lib/src/implemented_client.dart b/packages/wyatt_http_client/lib/src/implemented_client.dart new file mode 100644 index 00000000..a9790c33 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/implemented_client.dart @@ -0,0 +1,86 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +abstract class ImplementedClient extends BaseClient { + final Client _inner; + + ImplementedClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} diff --git a/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart new file mode 100644 index 00000000..b5931d48 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart @@ -0,0 +1,83 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:http/http.dart'; + +mixin BodyTransformer on Client { + Object? bodyMutator(Object? body); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.post( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.put( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.patch( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.delete( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart new file mode 100644 index 00000000..42cc270b --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart @@ -0,0 +1,131 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +mixin HeadersTransformer on Client { + Map? headersMutator( + Object? body, + Map? headers, + ); + + @override + Future head( + Uri url, { + Map? headers, + }) { + return super.head( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future get( + Uri url, { + Map? headers, + }) { + return super.get( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.post( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.put( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.patch( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.delete( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future read( + Uri url, { + Map? headers, + }) { + return super.read( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future readBytes( + Uri url, { + Map? headers, + }) { + return super.readBytes( + url, + headers: headersMutator(null, headers), + ); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart new file mode 100644 index 00000000..a1abf27a --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart @@ -0,0 +1,33 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +mixin Oauth2Transformer on ImplementedBaseClient { + late final String authorizationEndpoint; + late final String tokenEndpoint; + String? accessToken; + String? refreshToken; + + BaseRequest requestAuthenticator(BaseRequest request); + + @override + Future send(BaseRequest request) { + final req = requestAuthenticator(request); + return super.send(req); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart new file mode 100644 index 00000000..24db0982 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart @@ -0,0 +1,27 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +mixin RequestTransformer on ImplementedBaseClient { + BaseRequest requestMutator(BaseRequest request); + + @override + Future send(BaseRequest request) { + return super.send(requestMutator(request)); + } +} diff --git a/packages/wyatt_http_client/lib/src/rest_client.dart b/packages/wyatt_http_client/lib/src/rest_client.dart index cde6f3d6..1925eef1 100644 --- a/packages/wyatt_http_client/lib/src/rest_client.dart +++ b/packages/wyatt_http_client/lib/src/rest_client.dart @@ -14,25 +14,177 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:convert'; + import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; +import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/request_transformer.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; -class RestClient extends BaseClient { +class RestClient extends ImplementedBaseClient + with BodyTransformer, HeadersTransformer, RequestTransformer { final Protocols protocol; final String? authority; - final Client _inner; - RestClient({ this.protocol = Protocols.https, this.authority = '', - Client? inner, - }) : _inner = inner ?? Client(); + super.inner, + }); @override - Future send(BaseRequest request) { + Object? bodyMutator(Object? body) { + print( + 'RestClient::bodyMutator -> encode in json if body is Map: ${body is Map}', + ); + if (body is Map) { + return jsonEncode(body); + } + return body; + } + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print( + 'RestClient::headersMutator -> add json content-type if body is Map: ${body is Map}', + ); + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (body is Map) { + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } + return headers; + } + + @override + BaseRequest requestMutator(BaseRequest request) { + print( + 'RestClient::requestMutator -> add prefix path: ${protocol.scheme}$authority', + ); final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); - return _inner.send(Utils.copyRequestWith(request, url: uri)); + return Utils.copyRequestWith(request, url: uri); } } + +// class RestClient extends BaseClient { +// final Protocols protocol; +// final String? authority; + +// final Client _inner; + +// RestClient({ +// this.protocol = Protocols.https, +// this.authority = '', +// Client? inner, +// }) : _inner = inner ?? Client(); + +// String? forceJson(Object? body) { +// String? b; +// if (body != null && body is Map) { +// b = jsonEncode(body); +// } +// return b; +// } + +// Map? forceJsonHeader( +// Object? body, Map? headers,) { +// final Map h = headers ?? {}; +// if (body != null && body is Map) { +// h['Content-Type'] = 'application/json'; +// } +// return h; +// } + +// // @override +// // Future post( +// // Uri url, { +// // Map? headers, +// // Object? body, +// // Encoding? encoding, +// // }) { +// // final b = forceJson(body) ?? body; +// // final h = forceJsonHeader(body, headers) ?? headers; +// // print(b); +// // print(h); +// // return super.post( +// // url, +// // headers: h, +// // body: b, +// // encoding: encoding, +// // ); +// // } + +// @override +// Future put( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.put( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future patch( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.patch( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future delete( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.delete( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future send(BaseRequest request) { +// final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); +// return _inner.send( +// Utils.copyRequestWith( +// request, +// url: uri, +// ), +// ); +// } +// }