diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 7b27be8b20..a34d0d4830 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -3,6 +3,11 @@ .travis.yml README.md analysis_options.yaml +doc/APIKeyApi.md +doc/APIKeyCreateDto.md +doc/APIKeyCreateResponseDto.md +doc/APIKeyResponseDto.md +doc/APIKeyUpdateDto.md doc/AddAssetsDto.md doc/AddAssetsResponseDto.md doc/AddUsersDto.md @@ -85,6 +90,7 @@ doc/ValidateAccessTokenResponseDto.md git_push.sh lib/api.dart lib/api/album_api.dart +lib/api/api_key_api.dart lib/api/asset_api.dart lib/api/authentication_api.dart lib/api/device_info_api.dart @@ -109,6 +115,10 @@ lib/model/admin_signup_response_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/all_job_status_response_dto.dart +lib/model/api_key_create_dto.dart +lib/model/api_key_create_response_dto.dart +lib/model/api_key_response_dto.dart +lib/model/api_key_update_dto.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart @@ -180,6 +190,11 @@ test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart test/all_job_status_response_dto_test.dart +test/api_key_api_test.dart +test/api_key_create_dto_test.dart +test/api_key_create_response_dto_test.dart +test/api_key_response_dto_test.dart +test/api_key_update_dto_test.dart test/asset_api_test.dart test/asset_count_by_time_bucket_response_dto_test.dart test/asset_count_by_time_bucket_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e0dccdbf7c..27387bfe0f 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -39,22 +39,15 @@ Please follow the [installation procedure](#installation--usage) and then run th ```dart import 'package:openapi/api.dart'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); -final api_instance = AlbumApi(); -final albumId = albumId_example; // String | -final addAssetsDto = AddAssetsDto(); // AddAssetsDto | +final api_instance = APIKeyApi(); +final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | try { - final result = api_instance.addAssetsToAlbum(albumId, addAssetsDto); + final result = api_instance.createKey(aPIKeyCreateDto); print(result); } catch (e) { - print('Exception when calling AlbumApi->addAssetsToAlbum: $e\n'); + print('Exception when calling APIKeyApi->createKey: $e\n'); } ``` @@ -65,6 +58,11 @@ All URIs are relative to */api* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*APIKeyApi* | [**createKey**](doc//APIKeyApi.md#createkey) | **POST** /api-key | +*APIKeyApi* | [**deleteKey**](doc//APIKeyApi.md#deletekey) | **DELETE** /api-key/{id} | +*APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} | +*APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key | +*APIKeyApi* | [**updateKey**](doc//APIKeyApi.md#updatekey) | **PUT** /api-key/{id} | *AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{albumId}/assets | *AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users | *AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | @@ -138,6 +136,10 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [APIKeyCreateDto](doc//APIKeyCreateDto.md) + - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) + - [APIKeyResponseDto](doc//APIKeyResponseDto.md) + - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [AddAssetsDto](doc//AddAssetsDto.md) - [AddAssetsResponseDto](doc//AddAssetsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) diff --git a/mobile/openapi/doc/APIKeyApi.md b/mobile/openapi/doc/APIKeyApi.md new file mode 100644 index 0000000000..c715cedd8f --- /dev/null +++ b/mobile/openapi/doc/APIKeyApi.md @@ -0,0 +1,220 @@ +# openapi.api.APIKeyApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**createKey**](APIKeyApi.md#createkey) | **POST** /api-key | +[**deleteKey**](APIKeyApi.md#deletekey) | **DELETE** /api-key/{id} | +[**getKey**](APIKeyApi.md#getkey) | **GET** /api-key/{id} | +[**getKeys**](APIKeyApi.md#getkeys) | **GET** /api-key | +[**updateKey**](APIKeyApi.md#updatekey) | **PUT** /api-key/{id} | + + +# **createKey** +> APIKeyCreateResponseDto createKey(aPIKeyCreateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; + +final api_instance = APIKeyApi(); +final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | + +try { + final result = api_instance.createKey(aPIKeyCreateDto); + print(result); +} catch (e) { + print('Exception when calling APIKeyApi->createKey: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **aPIKeyCreateDto** | [**APIKeyCreateDto**](APIKeyCreateDto.md)| | + +### Return type + +[**APIKeyCreateResponseDto**](APIKeyCreateResponseDto.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **deleteKey** +> deleteKey(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; + +final api_instance = APIKeyApi(); +final id = 8.14; // num | + +try { + api_instance.deleteKey(id); +} catch (e) { + print('Exception when calling APIKeyApi->deleteKey: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **num**| | + +### Return type + +void (empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getKey** +> APIKeyResponseDto getKey(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; + +final api_instance = APIKeyApi(); +final id = 8.14; // num | + +try { + final result = api_instance.getKey(id); + print(result); +} catch (e) { + print('Exception when calling APIKeyApi->getKey: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **num**| | + +### Return type + +[**APIKeyResponseDto**](APIKeyResponseDto.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getKeys** +> List getKeys() + + + +### Example +```dart +import 'package:openapi/api.dart'; + +final api_instance = APIKeyApi(); + +try { + final result = api_instance.getKeys(); + print(result); +} catch (e) { + print('Exception when calling APIKeyApi->getKeys: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**List**](APIKeyResponseDto.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **updateKey** +> APIKeyResponseDto updateKey(id, aPIKeyUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; + +final api_instance = APIKeyApi(); +final id = 8.14; // num | +final aPIKeyUpdateDto = APIKeyUpdateDto(); // APIKeyUpdateDto | + +try { + final result = api_instance.updateKey(id, aPIKeyUpdateDto); + print(result); +} catch (e) { + print('Exception when calling APIKeyApi->updateKey: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **num**| | + **aPIKeyUpdateDto** | [**APIKeyUpdateDto**](APIKeyUpdateDto.md)| | + +### Return type + +[**APIKeyResponseDto**](APIKeyResponseDto.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/APIKeyCreateDto.md b/mobile/openapi/doc/APIKeyCreateDto.md new file mode 100644 index 0000000000..0355e3654f --- /dev/null +++ b/mobile/openapi/doc/APIKeyCreateDto.md @@ -0,0 +1,15 @@ +# openapi.model.APIKeyCreateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **String** | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/APIKeyCreateResponseDto.md b/mobile/openapi/doc/APIKeyCreateResponseDto.md new file mode 100644 index 0000000000..d0d4bea105 --- /dev/null +++ b/mobile/openapi/doc/APIKeyCreateResponseDto.md @@ -0,0 +1,16 @@ +# openapi.model.APIKeyCreateResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**secret** | **String** | | +**apiKey** | [**APIKeyResponseDto**](APIKeyResponseDto.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/APIKeyResponseDto.md b/mobile/openapi/doc/APIKeyResponseDto.md new file mode 100644 index 0000000000..f085aa0255 --- /dev/null +++ b/mobile/openapi/doc/APIKeyResponseDto.md @@ -0,0 +1,18 @@ +# openapi.model.APIKeyResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **num** | | +**name** | **String** | | +**createdAt** | **String** | | +**updatedAt** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/APIKeyUpdateDto.md b/mobile/openapi/doc/APIKeyUpdateDto.md new file mode 100644 index 0000000000..d5a72ed860 --- /dev/null +++ b/mobile/openapi/doc/APIKeyUpdateDto.md @@ -0,0 +1,15 @@ +# openapi.model.APIKeyUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 83ca7a388b..a7f434972b 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -27,6 +27,7 @@ part 'auth/oauth.dart'; part 'auth/http_basic_auth.dart'; part 'auth/http_bearer_auth.dart'; +part 'api/api_key_api.dart'; part 'api/album_api.dart'; part 'api/asset_api.dart'; part 'api/authentication_api.dart'; @@ -38,6 +39,10 @@ part 'api/system_config_api.dart'; part 'api/tag_api.dart'; part 'api/user_api.dart'; +part 'model/api_key_create_dto.dart'; +part 'model/api_key_create_response_dto.dart'; +part 'model/api_key_response_dto.dart'; +part 'model/api_key_update_dto.dart'; part 'model/add_assets_dto.dart'; part 'model/add_assets_response_dto.dart'; part 'model/add_users_dto.dart'; diff --git a/mobile/openapi/lib/api/api_key_api.dart b/mobile/openapi/lib/api/api_key_api.dart new file mode 100644 index 0000000000..26223bf891 --- /dev/null +++ b/mobile/openapi/lib/api/api_key_api.dart @@ -0,0 +1,249 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class APIKeyApi { + APIKeyApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /api-key' operation and returns the [Response]. + /// Parameters: + /// + /// * [APIKeyCreateDto] aPIKeyCreateDto (required): + Future createKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/api-key'; + + // ignore: prefer_final_locals + Object? postBody = aPIKeyCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [APIKeyCreateDto] aPIKeyCreateDto (required): + Future createKey(APIKeyCreateDto aPIKeyCreateDto,) async { + final response = await createKeyWithHttpInfo(aPIKeyCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyCreateResponseDto',) as APIKeyCreateResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /api-key/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [num] id (required): + Future deleteKeyWithHttpInfo(num id,) async { + // ignore: prefer_const_declarations + final path = r'/api-key/{id}' + .replaceAll('{id}', id.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [num] id (required): + Future deleteKey(num id,) async { + final response = await deleteKeyWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /api-key/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [num] id (required): + Future getKeyWithHttpInfo(num id,) async { + // ignore: prefer_const_declarations + final path = r'/api-key/{id}' + .replaceAll('{id}', id.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [num] id (required): + Future getKey(num id,) async { + final response = await getKeyWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /api-key' operation and returns the [Response]. + Future getKeysWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/api-key'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> getKeys() async { + final response = await getKeysWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + + /// Performs an HTTP 'PUT /api-key/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [num] id (required): + /// + /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): + Future updateKeyWithHttpInfo(num id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/api-key/{id}' + .replaceAll('{id}', id.toString()); + + // ignore: prefer_final_locals + Object? postBody = aPIKeyUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [num] id (required): + /// + /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): + Future updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + final response = await updateKeyWithHttpInfo(id, aPIKeyUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 13f5a3e02d..f3d0604b72 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -192,6 +192,14 @@ class ApiClient { return valueString == 'true' || valueString == '1'; case 'DateTime': return value is DateTime ? value : DateTime.tryParse(value); + case 'APIKeyCreateDto': + return APIKeyCreateDto.fromJson(value); + case 'APIKeyCreateResponseDto': + return APIKeyCreateResponseDto.fromJson(value); + case 'APIKeyResponseDto': + return APIKeyResponseDto.fromJson(value); + case 'APIKeyUpdateDto': + return APIKeyUpdateDto.fromJson(value); case 'AddAssetsDto': return AddAssetsDto.fromJson(value); case 'AddAssetsResponseDto': diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart new file mode 100644 index 0000000000..0cd0480b94 --- /dev/null +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -0,0 +1,120 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class APIKeyCreateDto { + /// Returns a new [APIKeyCreateDto] instance. + APIKeyCreateDto({ + this.name, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? name; + + @override + bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (name == null ? 0 : name!.hashCode); + + @override + String toString() => 'APIKeyCreateDto[name=$name]'; + + Map toJson() { + final _json = {}; + if (name != null) { + _json[r'name'] = name; + } else { + _json[r'name'] = null; + } + return _json; + } + + /// Returns a new [APIKeyCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static APIKeyCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "APIKeyCreateDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "APIKeyCreateDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return APIKeyCreateDto( + name: mapValueOfType(json, r'name'), + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = APIKeyCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of APIKeyCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyCreateDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart new file mode 100644 index 0000000000..5947f7ff0a --- /dev/null +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -0,0 +1,119 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class APIKeyCreateResponseDto { + /// Returns a new [APIKeyCreateResponseDto] instance. + APIKeyCreateResponseDto({ + required this.secret, + required this.apiKey, + }); + + String secret; + + APIKeyResponseDto apiKey; + + @override + bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateResponseDto && + other.secret == secret && + other.apiKey == apiKey; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (secret.hashCode) + + (apiKey.hashCode); + + @override + String toString() => 'APIKeyCreateResponseDto[secret=$secret, apiKey=$apiKey]'; + + Map toJson() { + final _json = {}; + _json[r'secret'] = secret; + _json[r'apiKey'] = apiKey; + return _json; + } + + /// Returns a new [APIKeyCreateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static APIKeyCreateResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "APIKeyCreateResponseDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "APIKeyCreateResponseDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return APIKeyCreateResponseDto( + secret: mapValueOfType(json, r'secret')!, + apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = APIKeyCreateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyCreateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of APIKeyCreateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'secret', + 'apiKey', + }; +} + diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart new file mode 100644 index 0000000000..6b1d93dbee --- /dev/null +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -0,0 +1,137 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class APIKeyResponseDto { + /// Returns a new [APIKeyResponseDto] instance. + APIKeyResponseDto({ + required this.id, + required this.name, + required this.createdAt, + required this.updatedAt, + }); + + num id; + + String name; + + String createdAt; + + String updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is APIKeyResponseDto && + other.id == id && + other.name == name && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (id.hashCode) + + (name.hashCode) + + (createdAt.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'APIKeyResponseDto[id=$id, name=$name, createdAt=$createdAt, updatedAt=$updatedAt]'; + + Map toJson() { + final _json = {}; + _json[r'id'] = id; + _json[r'name'] = name; + _json[r'createdAt'] = createdAt; + _json[r'updatedAt'] = updatedAt; + return _json; + } + + /// Returns a new [APIKeyResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static APIKeyResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "APIKeyResponseDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "APIKeyResponseDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return APIKeyResponseDto( + id: json[r'id'] == null + ? null + : num.parse(json[r'id'].toString()), + name: mapValueOfType(json, r'name')!, + createdAt: mapValueOfType(json, r'createdAt')!, + updatedAt: mapValueOfType(json, r'updatedAt')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = APIKeyResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of APIKeyResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyResponseDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'id', + 'name', + 'createdAt', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart new file mode 100644 index 0000000000..9116f84aa2 --- /dev/null +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -0,0 +1,111 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class APIKeyUpdateDto { + /// Returns a new [APIKeyUpdateDto] instance. + APIKeyUpdateDto({ + required this.name, + }); + + String name; + + @override + bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (name.hashCode); + + @override + String toString() => 'APIKeyUpdateDto[name=$name]'; + + Map toJson() { + final _json = {}; + _json[r'name'] = name; + return _json; + } + + /// Returns a new [APIKeyUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static APIKeyUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "APIKeyUpdateDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "APIKeyUpdateDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return APIKeyUpdateDto( + name: mapValueOfType(json, r'name')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = APIKeyUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of APIKeyUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = APIKeyUpdateDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'name', + }; +} + diff --git a/mobile/openapi/test/api_key_api_test.dart b/mobile/openapi/test/api_key_api_test.dart new file mode 100644 index 0000000000..588a6f4c20 --- /dev/null +++ b/mobile/openapi/test/api_key_api_test.dart @@ -0,0 +1,46 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for APIKeyApi +void main() { + // final instance = APIKeyApi(); + + group('tests for APIKeyApi', () { + //Future createKey(APIKeyCreateDto aPIKeyCreateDto) async + test('test createKey', () async { + // TODO + }); + + //Future deleteKey(num id) async + test('test deleteKey', () async { + // TODO + }); + + //Future getKey(num id) async + test('test getKey', () async { + // TODO + }); + + //Future> getKeys() async + test('test getKeys', () async { + // TODO + }); + + //Future updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto) async + test('test updateKey', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/api_key_create_dto_test.dart b/mobile/openapi/test/api_key_create_dto_test.dart new file mode 100644 index 0000000000..a09181ef0f --- /dev/null +++ b/mobile/openapi/test/api_key_create_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for APIKeyCreateDto +void main() { + // final instance = APIKeyCreateDto(); + + group('test APIKeyCreateDto', () { + // String name + test('to test the property `name`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/api_key_create_response_dto_test.dart b/mobile/openapi/test/api_key_create_response_dto_test.dart new file mode 100644 index 0000000000..6998ea6a92 --- /dev/null +++ b/mobile/openapi/test/api_key_create_response_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for APIKeyCreateResponseDto +void main() { + // final instance = APIKeyCreateResponseDto(); + + group('test APIKeyCreateResponseDto', () { + // String secret + test('to test the property `secret`', () async { + // TODO + }); + + // APIKeyResponseDto apiKey + test('to test the property `apiKey`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/api_key_response_dto_test.dart b/mobile/openapi/test/api_key_response_dto_test.dart new file mode 100644 index 0000000000..6862afa7ad --- /dev/null +++ b/mobile/openapi/test/api_key_response_dto_test.dart @@ -0,0 +1,42 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for APIKeyResponseDto +void main() { + // final instance = APIKeyResponseDto(); + + group('test APIKeyResponseDto', () { + // num id + test('to test the property `id`', () async { + // TODO + }); + + // String name + test('to test the property `name`', () async { + // TODO + }); + + // String createdAt + test('to test the property `createdAt`', () async { + // TODO + }); + + // String updatedAt + test('to test the property `updatedAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/api_key_update_dto_test.dart b/mobile/openapi/test/api_key_update_dto_test.dart new file mode 100644 index 0000000000..ca7bc2187e --- /dev/null +++ b/mobile/openapi/test/api_key_update_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for APIKeyUpdateDto +void main() { + // final instance = APIKeyUpdateDto(); + + group('test APIKeyUpdateDto', () { + // String name + test('to test the property `name`', () async { + // TODO + }); + + + }); + +} diff --git a/server/apps/immich/src/api-v1/api-key/api-key.controller.ts b/server/apps/immich/src/api-v1/api-key/api-key.controller.ts new file mode 100644 index 0000000000..ab2434cd48 --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/api-key.controller.ts @@ -0,0 +1,48 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, ValidationPipe } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; +import { Authenticated } from '../../decorators/authenticated.decorator'; +import { APIKeyService } from './api-key.service'; +import { APIKeyCreateDto } from './dto/api-key-create.dto'; +import { APIKeyUpdateDto } from './dto/api-key-update.dto'; +import { APIKeyCreateResponseDto } from './repsonse-dto/api-key-create-response.dto'; +import { APIKeyResponseDto } from './repsonse-dto/api-key-response.dto'; + +@ApiTags('API Key') +@Controller('api-key') +@Authenticated() +export class APIKeyController { + constructor(private service: APIKeyService) {} + + @Post() + createKey( + @GetAuthUser() authUser: AuthUserDto, + @Body(ValidationPipe) dto: APIKeyCreateDto, + ): Promise { + return this.service.create(authUser, dto); + } + + @Get() + getKeys(@GetAuthUser() authUser: AuthUserDto): Promise { + return this.service.getAll(authUser); + } + + @Get(':id') + getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise { + return this.service.getById(authUser, id); + } + + @Put(':id') + updateKey( + @GetAuthUser() authUser: AuthUserDto, + @Param('id', ParseIntPipe) id: number, + @Body(ValidationPipe) dto: APIKeyUpdateDto, + ): Promise { + return this.service.update(authUser, id, dto); + } + + @Delete(':id') + deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise { + return this.service.delete(authUser, id); + } +} diff --git a/server/apps/immich/src/api-v1/api-key/api-key.module.ts b/server/apps/immich/src/api-v1/api-key/api-key.module.ts new file mode 100644 index 0000000000..2f64e0262a --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/api-key.module.ts @@ -0,0 +1,16 @@ +import { APIKeyEntity } from '@app/database'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { APIKeyController } from './api-key.controller'; +import { APIKeyRepository, IKeyRepository } from './api-key.repository'; +import { APIKeyService } from './api-key.service'; + +const KEY_REPOSITORY = { provide: IKeyRepository, useClass: APIKeyRepository }; + +@Module({ + imports: [TypeOrmModule.forFeature([APIKeyEntity])], + controllers: [APIKeyController], + providers: [APIKeyService, KEY_REPOSITORY], + exports: [APIKeyService, KEY_REPOSITORY], +}) +export class APIKeyModule {} diff --git a/server/apps/immich/src/api-v1/api-key/api-key.repository.ts b/server/apps/immich/src/api-v1/api-key/api-key.repository.ts new file mode 100644 index 0000000000..ec203d4ea1 --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/api-key.repository.ts @@ -0,0 +1,59 @@ +import { APIKeyEntity } from '@app/database'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +export const IKeyRepository = 'IKeyRepository'; + +export interface IKeyRepository { + create(dto: Partial): Promise; + update(userId: string, id: number, dto: Partial): Promise; + delete(userId: string, id: number): Promise; + /** + * Includes the hashed `key` for verification + * @param id + */ + getKey(id: number): Promise; + getById(userId: string, id: number): Promise; + getByUserId(userId: string): Promise; +} + +@Injectable() +export class APIKeyRepository implements IKeyRepository { + constructor(@InjectRepository(APIKeyEntity) private repository: Repository) {} + + async create(dto: Partial): Promise { + return this.repository.save(dto); + } + + async update(userId: string, id: number, dto: Partial): Promise { + await this.repository.update({ userId, id }, dto); + return this.repository.findOneOrFail({ where: { id: dto.id } }); + } + + async delete(userId: string, id: number): Promise { + await this.repository.delete({ userId, id }); + } + + getKey(id: number): Promise { + return this.repository.findOne({ + select: { + id: true, + key: true, + userId: true, + }, + where: { id }, + relations: { + user: true, + }, + }); + } + + getById(userId: string, id: number): Promise { + return this.repository.findOne({ where: { userId, id } }); + } + + getByUserId(userId: string): Promise { + return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); + } +} diff --git a/server/apps/immich/src/api-v1/api-key/api-key.service.ts b/server/apps/immich/src/api-v1/api-key/api-key.service.ts new file mode 100644 index 0000000000..60d7249743 --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/api-key.service.ts @@ -0,0 +1,74 @@ +import { UserEntity } from '@app/database'; +import { BadRequestException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { compareSync, hash } from 'bcrypt'; +import { randomBytes } from 'node:crypto'; +import { AuthUserDto } from '../../decorators/auth-user.decorator'; +import { IKeyRepository } from './api-key.repository'; +import { APIKeyCreateDto } from './dto/api-key-create.dto'; +import { APIKeyCreateResponseDto } from './repsonse-dto/api-key-create-response.dto'; +import { APIKeyResponseDto, mapKey } from './repsonse-dto/api-key-response.dto'; + +@Injectable() +export class APIKeyService { + constructor(@Inject(IKeyRepository) private repository: IKeyRepository) {} + + async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise { + const key = randomBytes(24).toString('base64').replace(/\W/g, ''); + const entity = await this.repository.create({ + key: await hash(key, 10), + name: dto.name || 'API Key', + userId: authUser.id, + }); + + const secret = Buffer.from(`${entity.id}:${key}`, 'utf8').toString('base64'); + + return { secret, apiKey: mapKey(entity) }; + } + + async update(authUser: AuthUserDto, id: number, dto: APIKeyCreateDto): Promise { + const exists = await this.repository.getById(authUser.id, id); + if (!exists) { + throw new BadRequestException('API Key not found'); + } + + return this.repository.update(authUser.id, id, { + name: dto.name, + }); + } + + async delete(authUser: AuthUserDto, id: number): Promise { + const exists = await this.repository.getById(authUser.id, id); + if (!exists) { + throw new BadRequestException('API Key not found'); + } + + await this.repository.delete(authUser.id, id); + } + + async getById(authUser: AuthUserDto, id: number): Promise { + const key = await this.repository.getById(authUser.id, id); + if (!key) { + throw new BadRequestException('API Key not found'); + } + return mapKey(key); + } + + async getAll(authUser: AuthUserDto): Promise { + const keys = await this.repository.getByUserId(authUser.id); + return keys.map(mapKey); + } + + async validate(token: string): Promise { + const [_id, key] = Buffer.from(token, 'base64').toString('utf8').split(':'); + const id = Number(_id); + + if (id && key) { + const entity = await this.repository.getKey(id); + if (entity?.user && entity?.key && compareSync(key, entity.key)) { + return entity.user as UserEntity; + } + } + + throw new UnauthorizedException('Invalid API Key'); + } +} diff --git a/server/apps/immich/src/api-v1/api-key/dto/api-key-create.dto.ts b/server/apps/immich/src/api-v1/api-key/dto/api-key-create.dto.ts new file mode 100644 index 0000000000..30f5327363 --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/dto/api-key-create.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class APIKeyCreateDto { + @IsString() + @IsNotEmpty() + @IsOptional() + name?: string; +} diff --git a/server/apps/immich/src/api-v1/api-key/dto/api-key-update.dto.ts b/server/apps/immich/src/api-v1/api-key/dto/api-key-update.dto.ts new file mode 100644 index 0000000000..9db4f1ee5c --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/dto/api-key-update.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class APIKeyUpdateDto { + @IsString() + @IsNotEmpty() + name!: string; +} diff --git a/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-create-response.dto.ts b/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-create-response.dto.ts new file mode 100644 index 0000000000..f9f2825cce --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-create-response.dto.ts @@ -0,0 +1,6 @@ +import { APIKeyResponseDto } from './api-key-response.dto'; + +export class APIKeyCreateResponseDto { + secret!: string; + apiKey!: APIKeyResponseDto; +} diff --git a/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-response.dto.ts b/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-response.dto.ts new file mode 100644 index 0000000000..bc1b6b3127 --- /dev/null +++ b/server/apps/immich/src/api-v1/api-key/repsonse-dto/api-key-response.dto.ts @@ -0,0 +1,17 @@ +import { APIKeyEntity } from '@app/database'; + +export class APIKeyResponseDto { + id!: number; + name!: string; + createdAt!: string; + updatedAt!: string; +} + +export function mapKey(entity: APIKeyEntity): APIKeyResponseDto { + return { + id: entity.id, + name: entity.name, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; +} diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 504ddfbe17..0e3af7e119 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -19,6 +19,7 @@ import { JobModule } from './api-v1/job/job.module'; import { SystemConfigModule } from './api-v1/system-config/system-config.module'; import { OAuthModule } from './api-v1/oauth/oauth.module'; import { TagModule } from './api-v1/tag/tag.module'; +import { APIKeyModule } from './api-v1/api-key/api-key.module'; @Module({ imports: [ @@ -27,6 +28,8 @@ import { TagModule } from './api-v1/tag/tag.module'; DatabaseModule, UserModule, + APIKeyModule, + AssetModule, AuthModule, diff --git a/server/apps/immich/src/decorators/authenticated.decorator.ts b/server/apps/immich/src/decorators/authenticated.decorator.ts index 1543fc1bb2..6e3690e5fb 100644 --- a/server/apps/immich/src/decorators/authenticated.decorator.ts +++ b/server/apps/immich/src/decorators/authenticated.decorator.ts @@ -1,13 +1,13 @@ import { UseGuards } from '@nestjs/common'; import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware'; -import { JwtAuthGuard } from '../modules/immich-jwt/guards/jwt-auth.guard'; +import { AuthGuard } from '../modules/immich-jwt/guards/auth.guard'; interface AuthenticatedOptions { admin?: boolean; } export const Authenticated = (options?: AuthenticatedOptions) => { - const guards: Parameters = [JwtAuthGuard]; + const guards: Parameters = [AuthGuard]; options = options || {}; if (options.admin) { guards.push(AdminRolesGuard); diff --git a/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts index d555d90af7..ea22c0560e 100644 --- a/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts +++ b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts @@ -1,12 +1,17 @@ import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; import { Request } from 'express'; +import { UserResponseDto } from '../api-v1/user/response-dto/user-response.dto'; + +interface UserRequest extends Request { + user: UserResponseDto; +} @Injectable() export class AdminRolesGuard implements CanActivate { logger = new Logger(AdminRolesGuard.name); async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); const isAdmin = request.user?.isAdmin || false; if (!isAdmin) { this.logger.log(`Denied access to admin only route: ${request.path}`); diff --git a/server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts b/server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts new file mode 100644 index 0000000000..bea032615f --- /dev/null +++ b/server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; +import { API_KEY_STRATEGY } from '../strategies/api-key.strategy'; +import { JWT_STRATEGY } from '../strategies/jwt.strategy'; + +@Injectable() +export class AuthGuard extends PassportAuthGuard([JWT_STRATEGY, API_KEY_STRATEGY]) {} diff --git a/server/apps/immich/src/modules/immich-jwt/guards/jwt-auth.guard.ts b/server/apps/immich/src/modules/immich-jwt/guards/jwt-auth.guard.ts deleted file mode 100644 index 2155290ede..0000000000 --- a/server/apps/immich/src/modules/immich-jwt/guards/jwt-auth.guard.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts index aabfd650db..a7faf1def6 100644 --- a/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts +++ b/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts @@ -5,10 +5,12 @@ import { jwtConfig } from '../../config/jwt.config'; import { JwtStrategy } from './strategies/jwt.strategy'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UserEntity } from '@app/database'; +import { APIKeyModule } from '../../api-v1/api-key/api-key.module'; +import { APIKeyStrategy } from './strategies/api-key.strategy'; @Module({ - imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity])], - providers: [ImmichJwtService, JwtStrategy], + imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity]), APIKeyModule], + providers: [ImmichJwtService, JwtStrategy, APIKeyStrategy], exports: [ImmichJwtService], }) export class ImmichJwtModule {} diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts new file mode 100644 index 0000000000..fb6a222327 --- /dev/null +++ b/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { IStrategyOptions, Strategy } from 'passport-http-header-strategy'; +import { APIKeyService } from '../../../api-v1/api-key/api-key.service'; + +export const API_KEY_STRATEGY = 'api-key'; + +const options: IStrategyOptions = { + header: 'x-api-key', +}; + +@Injectable() +export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY) { + constructor(private apiKeyService: APIKeyService) { + super(options); + } + + async validate(token: string) { + return this.apiKeyService.validate(token); + } +} diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts index 50d99852d9..58720174a6 100644 --- a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts @@ -1,15 +1,17 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt'; import { Repository } from 'typeorm'; import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto'; import { UserEntity } from '@app/database'; import { jwtSecret } from '../../../constants/jwt.constant'; import { ImmichJwtService } from '../immich-jwt.service'; +export const JWT_STRATEGY = 'jwt'; + @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { +export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) { constructor( @InjectRepository(UserEntity) private usersRepository: Repository, @@ -22,7 +24,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ]), ignoreExpiration: false, secretOrKey: jwtSecret, - }); + } as StrategyOptions); } async validate(payload: JwtPayloadDto) { diff --git a/server/apps/immich/test/test-utils.ts b/server/apps/immich/test/test-utils.ts index b593cf06e1..c3fd091ad2 100644 --- a/server/apps/immich/test/test-utils.ts +++ b/server/apps/immich/test/test-utils.ts @@ -3,7 +3,7 @@ import { TestingModuleBuilder } from '@nestjs/testing'; import { DataSource } from 'typeorm'; import { IUserRepository } from '../src/api-v1/user/user-repository'; import { AuthUserDto } from '../src/decorators/auth-user.decorator'; -import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard'; +import { AuthGuard } from '../src/modules/immich-jwt/guards/auth.guard'; type CustomAuthCallback = () => AuthUserDto; @@ -49,5 +49,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa return true; }, }; - return builder.overrideGuard(JwtAuthGuard).useValue(canActivate); + return builder.overrideGuard(AuthGuard).useValue(canActivate); } diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts index 2b4322d2e3..ebb2a9be38 100644 --- a/server/apps/microservices/src/microservices.module.ts +++ b/server/apps/microservices/src/microservices.module.ts @@ -1,5 +1,5 @@ import { immichAppConfig, immichBullAsyncConfig } from '@app/common/config'; -import { DatabaseModule, AssetEntity, ExifEntity, SmartInfoEntity, UserEntity } from '@app/database'; +import { DatabaseModule, AssetEntity, ExifEntity, SmartInfoEntity, UserEntity, APIKeyEntity } from '@app/database'; import { StorageModule } from '@app/storage'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; @@ -23,7 +23,7 @@ import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.c ConfigModule.forRoot(immichAppConfig), DatabaseModule, ImmichConfigModule, - TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity]), + TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity, APIKeyEntity]), StorageModule, BullModule.forRootAsync(immichBullAsyncConfig), BullModule.registerQueue(...immichSharedQueues), diff --git a/server/apps/microservices/src/processors/user-deletion.processor.ts b/server/apps/microservices/src/processors/user-deletion.processor.ts index 2462d8978a..b44e630bdd 100644 --- a/server/apps/microservices/src/processors/user-deletion.processor.ts +++ b/server/apps/microservices/src/processors/user-deletion.processor.ts @@ -1,5 +1,5 @@ import { APP_UPLOAD_LOCATION, userUtils } from '@app/common'; -import { AssetEntity, UserEntity } from '@app/database'; +import { APIKeyEntity, AssetEntity, UserEntity } from '@app/database'; import { QueueNameEnum, userDeletionProcessorName } from '@app/job'; import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface'; import { Process, Processor } from '@nestjs/bull'; @@ -17,6 +17,9 @@ export class UserDeletionProcessor { @InjectRepository(AssetEntity) private assetRepository: Repository, + + @InjectRepository(APIKeyEntity) + private apiKeyRepository: Repository, ) {} @Process(userDeletionProcessorName) @@ -27,6 +30,7 @@ export class UserDeletionProcessor { const basePath = APP_UPLOAD_LOCATION; const userAssetDir = join(basePath, user.id); fs.rmSync(userAssetDir, { recursive: true, force: true }); + await this.apiKeyRepository.delete({ userId: user.id }); await this.assetRepository.delete({ userId: user.id }); await this.userRepository.remove(user); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 4ed31ace75..203929f3af 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -331,6 +331,148 @@ ] } }, + "/api-key": { + "post": { + "operationId": "createKey", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateResponseDto" + } + } + } + } + }, + "tags": [ + "API Key" + ] + }, + "get": { + "operationId": "getKeys", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/APIKeyResponseDto" + } + } + } + } + } + }, + "tags": [ + "API Key" + ] + } + }, + "/api-key/{id}": { + "get": { + "operationId": "getKey", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyResponseDto" + } + } + } + } + }, + "tags": [ + "API Key" + ] + }, + "put": { + "operationId": "updateKey", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyResponseDto" + } + } + } + } + }, + "tags": [ + "API Key" + ] + }, + "delete": { + "operationId": "deleteKey", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "API Key" + ] + } + }, "/asset/upload": { "post": { "operationId": "uploadFile", @@ -2467,6 +2609,63 @@ "profileImagePath" ] }, + "APIKeyCreateDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "APIKeyResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "createdAt", + "updatedAt" + ] + }, + "APIKeyCreateResponseDto": { + "type": "object", + "properties": { + "secret": { + "type": "string" + }, + "apiKey": { + "$ref": "#/components/schemas/APIKeyResponseDto" + } + }, + "required": [ + "secret", + "apiKey" + ] + }, + "APIKeyUpdateDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, "AssetFileUploadDto": { "type": "object", "properties": { diff --git a/server/libs/database/src/entities/api-key.entity.ts b/server/libs/database/src/entities/api-key.entity.ts new file mode 100644 index 0000000000..3b80480f6b --- /dev/null +++ b/server/libs/database/src/entities/api-key.entity.ts @@ -0,0 +1,26 @@ +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { UserEntity } from './user.entity'; + +@Entity('api_keys') +export class APIKeyEntity { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + name!: string; + + @Column({ select: false }) + key?: string; + + @Column() + userId!: string; + + @ManyToOne(() => UserEntity) + user?: UserEntity; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: string; +} diff --git a/server/libs/database/src/entities/index.ts b/server/libs/database/src/entities/index.ts index a11884ad25..f5edae663b 100644 --- a/server/libs/database/src/entities/index.ts +++ b/server/libs/database/src/entities/index.ts @@ -1,4 +1,5 @@ export * from './album.entity'; +export * from './api-key.entity'; export * from './asset-album.entity'; export * from './asset.entity'; export * from './device-info.entity'; diff --git a/server/libs/database/src/migrations/1672502270115-AddAPIKeys.ts b/server/libs/database/src/migrations/1672502270115-AddAPIKeys.ts new file mode 100644 index 0000000000..e72b3dc2fe --- /dev/null +++ b/server/libs/database/src/migrations/1672502270115-AddAPIKeys.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAPIKeys1672502270115 implements MigrationInterface { + name = 'AddAPIKeys1672502270115' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "api_keys" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`); + await queryRunner.query(`DROP TABLE "api_keys"`); + } + +} diff --git a/server/package-lock.json b/server/package-lock.json index c30de087bb..5dba867bb1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -47,6 +47,7 @@ "nest-commander": "^3.3.0", "openid-client": "^5.2.1", "passport": "^0.6.0", + "passport-http-header-strategy": "^1.1.0", "passport-jwt": "^4.0.0", "pg": "^8.7.1", "redis": "^3.1.2", @@ -2377,9 +2378,9 @@ } }, "node_modules/@types/inquirer": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.4.tgz", - "integrity": "sha512-Pxxx3i3AyK7vKAj3LRM/vF7ETcHKiLJ/u5CnNgbz/eYj/vB3xGAYtRxI5IKtq0hpe5iFHD22BKV3n6WHUu0k4Q==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==", "peer": true, "dependencies": { "@types/through": "*" @@ -8618,6 +8619,14 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-http-header-strategy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", + "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", + "dependencies": { + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-jwt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", @@ -9848,6 +9857,7 @@ "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, "node_modules/spawn-command": { @@ -13079,9 +13089,9 @@ } }, "@types/inquirer": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.4.tgz", - "integrity": "sha512-Pxxx3i3AyK7vKAj3LRM/vF7ETcHKiLJ/u5CnNgbz/eYj/vB3xGAYtRxI5IKtq0hpe5iFHD22BKV3n6WHUu0k4Q==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==", "peer": true, "requires": { "@types/through": "*" @@ -17917,6 +17927,14 @@ "utils-merge": "^1.0.1" } }, + "passport-http-header-strategy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", + "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", + "requires": { + "passport-strategy": "^1.0.0" + } + }, "passport-jwt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", diff --git a/server/package.json b/server/package.json index 6007a7150b..1cf4f4f7b2 100644 --- a/server/package.json +++ b/server/package.json @@ -70,6 +70,7 @@ "nest-commander": "^3.3.0", "openid-client": "^5.2.1", "passport": "^0.6.0", + "passport-http-header-strategy": "^1.1.0", "passport-jwt": "^4.0.0", "pg": "^8.7.1", "redis": "^3.1.2", diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 355171c4fa..061bc3ffdf 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -1,6 +1,7 @@ import { env } from '$env/dynamic/public'; import { AlbumApi, + APIKeyApi, AssetApi, AuthenticationApi, Configuration, @@ -21,6 +22,7 @@ class ImmichApi { public deviceInfoApi: DeviceInfoApi; public serverInfoApi: ServerInfoApi; public jobApi: JobApi; + public keyApi: APIKeyApi; public systemConfigApi: SystemConfigApi; private config = new Configuration({ basePath: '/api' }); @@ -34,6 +36,7 @@ class ImmichApi { this.deviceInfoApi = new DeviceInfoApi(this.config); this.serverInfoApi = new ServerInfoApi(this.config); this.jobApi = new JobApi(this.config); + this.keyApi = new APIKeyApi(this.config); this.systemConfigApi = new SystemConfigApi(this.config); } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 4876d051b4..7bfcdfe8b3 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -21,6 +21,82 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; +/** + * + * @export + * @interface APIKeyCreateDto + */ +export interface APIKeyCreateDto { + /** + * + * @type {string} + * @memberof APIKeyCreateDto + */ + 'name'?: string; +} +/** + * + * @export + * @interface APIKeyCreateResponseDto + */ +export interface APIKeyCreateResponseDto { + /** + * + * @type {string} + * @memberof APIKeyCreateResponseDto + */ + 'secret': string; + /** + * + * @type {APIKeyResponseDto} + * @memberof APIKeyCreateResponseDto + */ + 'apiKey': APIKeyResponseDto; +} +/** + * + * @export + * @interface APIKeyResponseDto + */ +export interface APIKeyResponseDto { + /** + * + * @type {number} + * @memberof APIKeyResponseDto + */ + 'id': number; + /** + * + * @type {string} + * @memberof APIKeyResponseDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof APIKeyResponseDto + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof APIKeyResponseDto + */ + 'updatedAt': string; +} +/** + * + * @export + * @interface APIKeyUpdateDto + */ +export interface APIKeyUpdateDto { + /** + * + * @type {string} + * @memberof APIKeyUpdateDto + */ + 'name': string; +} /** * * @export @@ -1990,6 +2066,363 @@ export interface ValidateAccessTokenResponseDto { 'authStatus': boolean; } +/** + * APIKeyApi - axios parameter creator + * @export + */ +export const APIKeyApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {APIKeyCreateDto} aPIKeyCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createKey: async (aPIKeyCreateDto: APIKeyCreateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'aPIKeyCreateDto' is not null or undefined + assertParamExists('createKey', 'aPIKeyCreateDto', aPIKeyCreateDto) + const localVarPath = `/api-key`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(aPIKeyCreateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteKey: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteKey', 'id', id) + const localVarPath = `/api-key/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getKey: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getKey', 'id', id) + const localVarPath = `/api-key/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getKeys: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api-key`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} id + * @param {APIKeyUpdateDto} aPIKeyUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateKey: async (id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateKey', 'id', id) + // verify required parameter 'aPIKeyUpdateDto' is not null or undefined + assertParamExists('updateKey', 'aPIKeyUpdateDto', aPIKeyUpdateDto) + const localVarPath = `/api-key/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(aPIKeyUpdateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * APIKeyApi - functional programming interface + * @export + */ +export const APIKeyApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = APIKeyApiAxiosParamCreator(configuration) + return { + /** + * + * @param {APIKeyCreateDto} aPIKeyCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createKey(aPIKeyCreateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteKey(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getKey(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getKeys(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getKeys(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {number} id + * @param {APIKeyUpdateDto} aPIKeyUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateKey(id, aPIKeyUpdateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * APIKeyApi - factory interface + * @export + */ +export const APIKeyApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = APIKeyApiFp(configuration) + return { + /** + * + * @param {APIKeyCreateDto} aPIKeyCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: any): AxiosPromise { + return localVarFp.createKey(aPIKeyCreateDto, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteKey(id: number, options?: any): AxiosPromise { + return localVarFp.deleteKey(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getKey(id: number, options?: any): AxiosPromise { + return localVarFp.getKey(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getKeys(options?: any): AxiosPromise> { + return localVarFp.getKeys(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {number} id + * @param {APIKeyUpdateDto} aPIKeyUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise { + return localVarFp.updateKey(id, aPIKeyUpdateDto, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * APIKeyApi - object-oriented interface + * @export + * @class APIKeyApi + * @extends {BaseAPI} + */ +export class APIKeyApi extends BaseAPI { + /** + * + * @param {APIKeyCreateDto} aPIKeyCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof APIKeyApi + */ + public createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: AxiosRequestConfig) { + return APIKeyApiFp(this.configuration).createKey(aPIKeyCreateDto, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof APIKeyApi + */ + public deleteKey(id: number, options?: AxiosRequestConfig) { + return APIKeyApiFp(this.configuration).deleteKey(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof APIKeyApi + */ + public getKey(id: number, options?: AxiosRequestConfig) { + return APIKeyApiFp(this.configuration).getKey(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof APIKeyApi + */ + public getKeys(options?: AxiosRequestConfig) { + return APIKeyApiFp(this.configuration).getKeys(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {number} id + * @param {APIKeyUpdateDto} aPIKeyUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof APIKeyApi + */ + public updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) { + return APIKeyApiFp(this.configuration).updateKey(id, aPIKeyUpdateDto, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * AlbumApi - axios parameter creator * @export diff --git a/web/src/lib/components/forms/api-key-form.svelte b/web/src/lib/components/forms/api-key-form.svelte new file mode 100644 index 0000000000..23a0c90af8 --- /dev/null +++ b/web/src/lib/components/forms/api-key-form.svelte @@ -0,0 +1,57 @@ + + + handleCancel()}> +
+
+ +

+ {title} +

+
+ +
handleSubmit()} autocomplete="off"> +
+ + +
+ +
+ + +
+
+
+
diff --git a/web/src/lib/components/forms/api-key-secret.svelte b/web/src/lib/components/forms/api-key-secret.svelte new file mode 100644 index 0000000000..7a37bb6c60 --- /dev/null +++ b/web/src/lib/components/forms/api-key-secret.svelte @@ -0,0 +1,69 @@ + + + +
+
+ +

+ API Key +

+ +

+ This value will only be shown once. Please be sure to copy it before closing the window. +

+
+ +
+ +