diff --git a/README.md b/README.md index 5d86542..7d3d7a6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,32 @@ final client = UnsplashClient( > :warning: When you are done using a client instance, make sure to call it's > `close` method. +### Moving Unsplash authentication to an authentication proxy + +If you would like to use an authentication proxy and want to change the URL of the +Unsplash API endpoint to your proxy URL you can use the following code: + +```dart +final client = UnsplashClient( + settings: const ClientSettings()), + proxyBaseUrl: Uri.parse('YOUR_PROXY_URL')); +); +``` +You can omit the `credentials` parameter and provide your own endpoint by +using the `proxyBaseUrl` parameter. + +If your authentication proxy requires authentication as well you can authenticate using a `bearer token` like so: +```dart +final client = UnsplashClient( + settings: const ClientSettings(bearerToken: 'YOUR_BEARER_TOKEN')), + proxyBaseUrl: Uri.parse('YOUR_PROXY_URL')); +); +``` +The bearer token will be assembled to a HTTP header like so: +``` +Authorization: Bearer YOUR_BEARER_TOKEN +``` + ## Get a random photo ```dart diff --git a/example/lib/main.dart b/example/lib/main.dart index adbdaa0..a73db1d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,7 +17,7 @@ void main(List args) async { // Create a client. final client = UnsplashClient( - settings: ClientSettings(credentials: appCredentials), + settings: ClientSettings(unsplashCredentials: appCredentials), ); // Fetch 5 random photos by calling `goAndGet` to execute the [Request] diff --git a/lib/src/client.dart b/lib/src/client.dart index cebab1a..cf0b0f9 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -18,15 +18,20 @@ import 'users.dart'; class ClientSettings { /// Creates new [ClientSettings]. /// - /// [credentials] must not be `null`. + /// [unsplashCredentials] must not be `null` if using no proxy URL. + /// [bearerToken] must not be 'null' if using a proxy that requires authentication const ClientSettings({ - required this.credentials, + this.unsplashCredentials, + this.bearerToken, this.debug = false, this.loggerName = 'unsplash_client', }); /// The credentials used by the [UnsplashClient] to authenticate the app. - final AppCredentials credentials; + /// These can be null in case a proxy for authentication is being used + final AppCredentials? unsplashCredentials; + + final String? bearerToken; /// Whether to log debug information. @Deprecated( @@ -47,15 +52,17 @@ class ClientSettings { identical(this, other) || other is ClientSettings && runtimeType == other.runtimeType && - credentials == other.credentials && + unsplashCredentials == other.unsplashCredentials && + bearerToken == other.bearerToken && maxPageSize == other.maxPageSize; @override - int get hashCode => credentials.hashCode ^ maxPageSize.hashCode; + int get hashCode => unsplashCredentials.hashCode ^ bearerToken.hashCode ^ maxPageSize.hashCode; @override String toString() { - return 'ClientSettings{credentials: $credentials, ' + return 'ClientSettings{credentials: $unsplashCredentials, ' + 'bearerToken: $bearerToken, ' 'maxPageSize: $maxPageSize}'; } } @@ -71,17 +78,19 @@ class UnsplashClient { /// [settings] must not be `null`. /// /// If no [httpClient] is provided, one is created. - UnsplashClient({ - required this.settings, - http.Client? httpClient, - }) : _http = httpClient ?? http.Client(), - logger = Logger(settings.loggerName); + /// + /// If no [proxyBaseUrl] is provided the Unsplash standard base URL + /// https://api.unsplash.com/ is used + UnsplashClient({required this.settings, http.Client? httpClient, Uri? proxyBaseUrl}) + : _http = httpClient ?? http.Client(), + logger = Logger(settings.loggerName), + baseUrl = proxyBaseUrl ?? Uri.parse('https://api.unsplash.com/'); /// The [Logger] used by this instance. final Logger logger; - /// The base url of the unsplash api. - final Uri baseUrl = Uri.parse('https://api.unsplash.com/'); + /// The base url of the unsplash api or its proxy. + final Uri baseUrl; /// The [ClientSettings] used by this client. final ClientSettings settings; @@ -206,9 +215,7 @@ class Request { // ignore: avoid_catches_without_on_clauses } - if (httpResponse.statusCode < 400 && - json != null && - bodyDeserializer != null) { + if (httpResponse.statusCode < 400 && json != null && bodyDeserializer != null) { data = bodyDeserializer!(json); } @@ -259,7 +266,11 @@ class Request { // Auth // TODO implement oauth assert(isPublicAction); - headers.addAll(_publicActionAuthHeader(client.settings.credentials)); + if (client.settings.unsplashCredentials != null) { + headers.addAll(_publicActionAuthHeader(client.settings.unsplashCredentials!)); + } else if (client.settings.bearerToken != null) { + headers.addAll(_bearerTokenAuthHeader(client.settings.bearerToken!)); + } return headers; } @@ -277,11 +288,7 @@ class Request { @override int get hashCode => - client.hashCode ^ - httpRequest.hashCode ^ - jsonBody.hashCode ^ - isPublicAction.hashCode ^ - bodyDeserializer.hashCode; + client.hashCode ^ httpRequest.hashCode ^ jsonBody.hashCode ^ isPublicAction.hashCode ^ bodyDeserializer.hashCode; @override String toString() { @@ -361,12 +368,7 @@ class Response { @override int get hashCode => - request.hashCode ^ - httpRequest.hashCode ^ - httpResponse.hashCode ^ - body.hashCode ^ - json.hashCode ^ - data.hashCode; + request.hashCode ^ httpRequest.hashCode ^ httpResponse.hashCode ^ body.hashCode ^ json.hashCode ^ data.hashCode; @override String toString() { @@ -388,6 +390,10 @@ Map _publicActionAuthHeader(AppCredentials credentials) { return {'Authorization': 'Client-ID ${credentials.accessKey}'}; } +Map _bearerTokenAuthHeader(String bearerToken) { + return {'Authorization': 'Bearer $bearerToken'}; +} + Map _sanitizeHeaders(Map headers) { return headers.map((key, value) { final isAuthorization = key.toLowerCase() == 'authorization'; @@ -410,7 +416,5 @@ ${_printHeaders(response.headers)} } String _printHeaders(Map headers) { - return headers.entries - .map((header) => '${header.key}: ${header.value}') - .join('\n'); + return headers.entries.map((header) => '${header.key}: ${header.value}').join('\n'); } diff --git a/test/client_test.dart b/test/client_test.dart index 60e9306..f71a683 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -2,11 +2,10 @@ import 'package:test/test.dart'; import 'package:unsplash_client/unsplash_client.dart'; void main() { - test('executing requests after client has been closed should throw', - () async { + test('executing requests after client has been closed should throw', () async { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), @@ -19,4 +18,47 @@ void main() { expect(req.go, throwsStateError); }); + + test('standard client functions work without app credentials as expected', () { + final clientWithUnsplashCredentials = UnsplashClient( + settings: const ClientSettings( + unsplashCredentials: AppCredentials( + secretKey: '1234', + accessKey: '5678', + ), + ), + ); + + final clientWithoutUnsplashCredentials = UnsplashClient( + settings: const ClientSettings(bearerToken: '1234'), + ); + + expect(clientWithUnsplashCredentials.settings == clientWithUnsplashCredentials.settings, true); + expect(clientWithUnsplashCredentials.settings == clientWithoutUnsplashCredentials.settings, false); + expect(clientWithoutUnsplashCredentials.settings == clientWithUnsplashCredentials.settings, false); + expect(clientWithoutUnsplashCredentials.settings == clientWithoutUnsplashCredentials.settings, true); + + var clientWithSettingsHashCode = clientWithUnsplashCredentials.settings.hashCode; + expect(clientWithSettingsHashCode == 1043090417, true); + + var clientWithoutUnsplashCredentialsHashCode = clientWithoutUnsplashCredentials.settings.hashCode; + expect(clientWithoutUnsplashCredentialsHashCode == 759431771, true); + + var clientWithUnsplashCredentialsString = clientWithUnsplashCredentials.settings.toString(); + expect( + clientWithUnsplashCredentialsString == + 'ClientSettings{credentials: Credentials{accessKey: 5678, ' + 'secretKey: HIDDEN}, ' + 'bearerToken: null, ' + 'maxPageSize: 30}', + true); + + var clientWithoutUnsplashCredentialsString = clientWithoutUnsplashCredentials.settings.toString(); + expect( + clientWithoutUnsplashCredentialsString == + 'ClientSettings{credentials: null, ' + 'bearerToken: 1234, ' + 'maxPageSize: 30}', + true); + }); } diff --git a/test/collections_test.dart b/test/collections_test.dart index 224e12b..c533f77 100644 --- a/test/collections_test.dart +++ b/test/collections_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/integration/integration_test_utils.dart b/test/integration/integration_test_utils.dart index 44062eb..2f0076f 100644 --- a/test/integration/integration_test_utils.dart +++ b/test/integration/integration_test_utils.dart @@ -15,8 +15,7 @@ void setupIntegrationTests(String name) { } _hasSetupIntegrationTests = true; - _recordedExpectations = - File.fromUri(_recordedExpectationsDir.uri.resolve('$name.json')); + _recordedExpectations = File.fromUri(_recordedExpectationsDir.uri.resolve('$name.json')); _setupRecordedExpectationsTestHooks(); _setupTestClientTestHooks(); @@ -26,11 +25,9 @@ bool get isCI => Platform.environment['CI'] != null; // === Recorded Expectations =================================================== -bool get _updateRecordedExpectations => - Platform.environment['UPDATE_RECORDED_EXPECTATIONS'] != null; +bool get _updateRecordedExpectations => Platform.environment['UPDATE_RECORDED_EXPECTATIONS'] != null; -final _recordedExpectationsDir = - Directory('test/fixtures/recorded_expectations'); +final _recordedExpectationsDir = Directory('test/fixtures/recorded_expectations'); late File _recordedExpectations; late MockServer mockServer; @@ -49,8 +46,7 @@ void _setupRecordedExpectationsTestHooks() { 'There are no recorded expectations.', ); - final recordedExpectations = - decodeExpectations(await _recordedExpectations.readAsString()); + final recordedExpectations = decodeExpectations(await _recordedExpectations.readAsString()); // If the content of `User-Agent` changes we can still use the recorded // expectations, since the header does not change the expected response. @@ -87,8 +83,7 @@ void _setupRecordedExpectationsTestHooks() { await _recordedExpectations.parent.create(recursive: true); final jsonEncoder = const JsonEncoder.withIndent(' '); - await _recordedExpectations - .writeAsString(jsonEncoder.convert(recordedExpectations)); + await _recordedExpectations.writeAsString(jsonEncoder.convert(recordedExpectations)); } mockServer.close(); @@ -141,12 +136,10 @@ void _setupTestClientTestHooks() { setUpAll(() async { // In CI we run tests always against recorded responses and need not // credentials. - final credentials = isCI - ? AppCredentials(accessKey: '', secretKey: '') - : await getTestAppCredentials(); + final credentials = isCI ? AppCredentials(accessKey: '', secretKey: '') : await getTestAppCredentials(); client = UnsplashClient( - settings: ClientSettings(credentials: credentials), + settings: ClientSettings(unsplashCredentials: credentials), httpClient: IOClient(mockServer.createProxiedHttpClient()), ); }); diff --git a/test/photos_test.dart b/test/photos_test.dart index a5aeebd..3365ede 100644 --- a/test/photos_test.dart +++ b/test/photos_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/search_test.dart b/test/search_test.dart index 6ec71b1..56d99ed 100644 --- a/test/search_test.dart +++ b/test/search_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/stats_test.dart b/test/stats_test.dart index 4c8ec02..330807f 100644 --- a/test/stats_test.dart +++ b/test/stats_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/topics_test.dart b/test/topics_test.dart index 92bf458..dcff861 100644 --- a/test/topics_test.dart +++ b/test/topics_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/users_test.dart b/test/users_test.dart index 63050b0..65eb46e 100644 --- a/test/users_test.dart +++ b/test/users_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ),