feat(web,server): api keys (#1244)

* feat(server): api keys

* chore: open-api

* feat(web): api keys

* fix: remove keys when deleting a user
This commit is contained in:
Jason Rasmussen 2023-01-02 15:22:33 -05:00 committed by GitHub
parent 9edbff0ec0
commit 9e6d6b2532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2586 additions and 35 deletions

View File

@ -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

View File

@ -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<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('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)

220
mobile/openapi/doc/APIKeyApi.md generated Normal file
View File

@ -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<APIKeyResponseDto> 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>**](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)

15
mobile/openapi/doc/APIKeyCreateDto.md generated Normal file
View File

@ -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)

View File

@ -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)

18
mobile/openapi/doc/APIKeyResponseDto.md generated Normal file
View File

@ -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)

15
mobile/openapi/doc/APIKeyUpdateDto.md generated Normal file
View File

@ -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)

View File

@ -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';

249
mobile/openapi/lib/api/api_key_api.dart generated Normal file
View File

@ -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<Response> createKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/api-key';
// ignore: prefer_final_locals
Object? postBody = aPIKeyCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [APIKeyCreateDto] aPIKeyCreateDto (required):
Future<APIKeyCreateResponseDto?> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [num] id (required):
Future<void> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [num] id (required):
Future<APIKeyResponseDto?> 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<Response> getKeysWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/api-key';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<APIKeyResponseDto>?> 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<APIKeyResponseDto>') as List)
.cast<APIKeyResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PUT /api-key/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [num] id (required):
///
/// * [APIKeyUpdateDto] aPIKeyUpdateDto (required):
Future<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [num] id (required):
///
/// * [APIKeyUpdateDto] aPIKeyUpdateDto (required):
Future<APIKeyResponseDto?> 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;
}
}

View File

@ -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':

View File

@ -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<String, dynamic> toJson() {
final _json = <String, dynamic>{};
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<String, dynamic>();
// 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<String>(json, r'name'),
);
}
return null;
}
static List<APIKeyCreateDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <APIKeyCreateDto>[];
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<String, APIKeyCreateDto> mapFromJson(dynamic json) {
final map = <String, APIKeyCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<APIKeyCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<APIKeyCreateDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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 = <String>{
};
}

View File

@ -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<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_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<String, dynamic>();
// 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<String>(json, r'secret')!,
apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!,
);
}
return null;
}
static List<APIKeyCreateResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <APIKeyCreateResponseDto>[];
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<String, APIKeyCreateResponseDto> mapFromJson(dynamic json) {
final map = <String, APIKeyCreateResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<APIKeyCreateResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<APIKeyCreateResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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 = <String>{
'secret',
'apiKey',
};
}

View File

@ -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<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_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<String, dynamic>();
// 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<String>(json, r'name')!,
createdAt: mapValueOfType<String>(json, r'createdAt')!,
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
);
}
return null;
}
static List<APIKeyResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <APIKeyResponseDto>[];
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<String, APIKeyResponseDto> mapFromJson(dynamic json) {
final map = <String, APIKeyResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<APIKeyResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<APIKeyResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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 = <String>{
'id',
'name',
'createdAt',
'updatedAt',
};
}

View File

@ -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<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_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<String, dynamic>();
// 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<String>(json, r'name')!,
);
}
return null;
}
static List<APIKeyUpdateDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <APIKeyUpdateDto>[];
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<String, APIKeyUpdateDto> mapFromJson(dynamic json) {
final map = <String, APIKeyUpdateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<APIKeyUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<APIKeyUpdateDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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 = <String>{
'name',
};
}

View File

@ -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<APIKeyCreateResponseDto> createKey(APIKeyCreateDto aPIKeyCreateDto) async
test('test createKey', () async {
// TODO
});
//Future deleteKey(num id) async
test('test deleteKey', () async {
// TODO
});
//Future<APIKeyResponseDto> getKey(num id) async
test('test getKey', () async {
// TODO
});
//Future<List<APIKeyResponseDto>> getKeys() async
test('test getKeys', () async {
// TODO
});
//Future<APIKeyResponseDto> updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto) async
test('test updateKey', () async {
// TODO
});
});
}

View File

@ -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
});
});
}

View File

@ -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
});
});
}

View File

@ -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
});
});
}

View File

@ -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
});
});
}

View File

@ -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<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto);
}
@Get()
getKeys(@GetAuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser);
}
@Get(':id')
getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
}
@Put(':id')
updateKey(
@GetAuthUser() authUser: AuthUserDto,
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) dto: APIKeyUpdateDto,
): Promise<APIKeyResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<void> {
return this.service.delete(authUser, id);
}
}

View File

@ -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 {}

View File

@ -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<APIKeyEntity>): Promise<APIKeyEntity>;
update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
delete(userId: string, id: number): Promise<void>;
/**
* Includes the hashed `key` for verification
* @param id
*/
getKey(id: number): Promise<APIKeyEntity | null>;
getById(userId: string, id: number): Promise<APIKeyEntity | null>;
getByUserId(userId: string): Promise<APIKeyEntity[]>;
}
@Injectable()
export class APIKeyRepository implements IKeyRepository {
constructor(@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>) {}
async create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> {
return this.repository.save(dto);
}
async update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> {
await this.repository.update({ userId, id }, dto);
return this.repository.findOneOrFail({ where: { id: dto.id } });
}
async delete(userId: string, id: number): Promise<void> {
await this.repository.delete({ userId, id });
}
getKey(id: number): Promise<APIKeyEntity | null> {
return this.repository.findOne({
select: {
id: true,
key: true,
userId: true,
},
where: { id },
relations: {
user: true,
},
});
}
getById(userId: string, id: number): Promise<APIKeyEntity | null> {
return this.repository.findOne({ where: { userId, id } });
}
getByUserId(userId: string): Promise<APIKeyEntity[]> {
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
}
}

View File

@ -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<APIKeyCreateResponseDto> {
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<APIKeyResponseDto> {
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<void> {
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<APIKeyResponseDto> {
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<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(authUser.id);
return keys.map(mapKey);
}
async validate(token: string): Promise<UserEntity> {
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');
}
}

View File

@ -0,0 +1,8 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class APIKeyCreateDto {
@IsString()
@IsNotEmpty()
@IsOptional()
name?: string;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class APIKeyUpdateDto {
@IsString()
@IsNotEmpty()
name!: string;
}

View File

@ -0,0 +1,6 @@
import { APIKeyResponseDto } from './api-key-response.dto';
export class APIKeyCreateResponseDto {
secret!: string;
apiKey!: APIKeyResponseDto;
}

View File

@ -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,
};
}

View File

@ -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,

View File

@ -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<typeof UseGuards> = [JwtAuthGuard];
const guards: Parameters<typeof UseGuards> = [AuthGuard];
options = options || {};
if (options.admin) {
guards.push(AdminRolesGuard);

View File

@ -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<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const request = context.switchToHttp().getRequest<UserRequest>();
const isAdmin = request.user?.isAdmin || false;
if (!isAdmin) {
this.logger.log(`Denied access to admin only route: ${request.path}`);

View File

@ -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]) {}

View File

@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@ -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 {}

View File

@ -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);
}
}

View File

@ -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<UserEntity>,
@ -22,7 +24,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
]),
ignoreExpiration: false,
secretOrKey: jwtSecret,
});
} as StrategyOptions);
}
async validate(payload: JwtPayloadDto) {

View File

@ -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);
}

View File

@ -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),

View File

@ -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<AssetEntity>,
@InjectRepository(APIKeyEntity)
private apiKeyRepository: Repository<APIKeyEntity>,
) {}
@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);
}

View File

@ -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": {

View File

@ -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;
}

View File

@ -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';

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAPIKeys1672502270115 implements MigrationInterface {
name = 'AddAPIKeys1672502270115'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
await queryRunner.query(`DROP TABLE "api_keys"`);
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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);
}

View File

@ -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<RequestArgs> => {
// 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<RequestArgs> => {
// 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<RequestArgs> => {
// 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<RequestArgs> => {
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<RequestArgs> => {
// 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<APIKeyCreateResponseDto>> {
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<void>> {
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<APIKeyResponseDto>> {
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<Array<APIKeyResponseDto>>> {
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<APIKeyResponseDto>> {
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<APIKeyCreateResponseDto> {
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<void> {
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<APIKeyResponseDto> {
return localVarFp.getKey(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getKeys(options?: any): AxiosPromise<Array<APIKeyResponseDto>> {
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<APIKeyResponseDto> {
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

View File

@ -0,0 +1,57 @@
<script lang="ts">
import { APIKeyResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
export let apiKey: Partial<APIKeyResponseDto>;
export let title = 'API Key';
export let cancelText = 'Cancel';
export let submitText = 'Save';
const dispatch = createEventDispatcher();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { ...apiKey, name: apiKey.name });
</script>
<FullScreenModal on:clickOutside={() => handleCancel()}>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<KeyVariant size="4em" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
{title}
</h1>
</div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Name</label>
<input
class="immich-form-input"
id="name"
name="name"
type="text"
bind:value={apiKey.name}
/>
</div>
<div class="flex w-full px-4 gap-4 mt-8">
<button
type="button"
on:click={() => handleCancel()}
class="flex-1 transition-colors bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-6 py-3 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium"
>{cancelText}
</button>
<button
type="submit"
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>{submitText}</button
>
</div>
</form>
</div>
</FullScreenModal>

View File

@ -0,0 +1,69 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
import { handleError } from '../../utils/handle-error';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
export let secret = '';
const dispatch = createEventDispatcher();
const handleDone = () => dispatch('done');
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(secret);
notificationController.show({
message: 'Copied to clipboard!',
type: NotificationType.Info
});
} catch (error) {
handleError(error, 'Unable to copy to clipboard');
}
};
</script>
<FullScreenModal>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<KeyVariant size="4em" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
API Key
</h1>
<p class="text-sm dark:text-immich-dark-fg">
This value will only be shown once. Please be sure to copy it before closing the window.
</p>
</div>
<div class="m-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="email">API Key</label> -->
<textarea
class="immich-form-input"
id="secret"
name="secret"
readonly={true}
value={secret}
/>
</div>
<div class="flex w-full px-4 gap-4 mt-8">
<button
on:click={() => handleCopy()}
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Copy to Clipboard</button
>
<button
on:click={() => handleDone()}
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Done</button
>
</div>
</div>
</FullScreenModal>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
export let title = 'Confirm Delete';
export let prompt = 'Are you sure you want to delete this item?';
export let confirmText = 'Confirm';
export let cancelText = 'Cancel';
const dispatch = createEventDispatcher();
const handleCancel = () => dispatch('cancel');
const handleConfirm = () => dispatch('confirm');
</script>
<FullScreenModal on:clickOutside={() => handleCancel()}>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
{title}
</h1>
</div>
<div>
<p class="ml-4 text-md py-5 text-center">{prompt}</p>
<div class="flex w-full px-4 gap-4 mt-4">
<button
on:click={() => handleCancel()}
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>
{cancelText}
</button>
<button
on:click={() => handleConfirm()}
class="flex-1 transition-colors bg-red-500 hover:bg-red-400 px-6 py-3 text-white rounded-full w-full font-medium"
>
{confirmText}
</button>
</div>
</div>
</div>
</FullScreenModal>

View File

@ -0,0 +1,180 @@
<script lang="ts">
import { api, APIKeyResponseDto } from '@api';
import { onMount } from 'svelte';
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import APIKeyForm from '../forms/api-key-form.svelte';
import APIKeySecret from '../forms/api-key-secret.svelte';
import DeleteConfirmDialogue from '../shared-components/delete-confirm-dialogue.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
let keys: APIKeyResponseDto[] = [];
let newKey: Partial<APIKeyResponseDto> | null = null;
let editKey: APIKeyResponseDto | null = null;
let deleteKey: APIKeyResponseDto | null = null;
let secret = '';
const locale = navigator.language;
const format: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric'
};
onMount(() => {
refreshKeys();
});
async function refreshKeys() {
const { data } = await api.keyApi.getKeys();
keys = data;
}
const handleCreate = async (event: CustomEvent<APIKeyResponseDto>) => {
try {
const dto = event.detail;
const { data } = await api.keyApi.createKey(dto);
secret = data.secret;
} catch (error) {
handleError(error, 'Unable to create a new API Key');
} finally {
await refreshKeys();
newKey = null;
}
};
const handleUpdate = async (event: CustomEvent<APIKeyResponseDto>) => {
if (!editKey) {
return;
}
const dto = event.detail;
try {
await api.keyApi.updateKey(editKey.id, { name: dto.name });
notificationController.show({
message: `Saved API Key`,
type: NotificationType.Info
});
} catch (error) {
handleError(error, 'Unable to save API Key');
} finally {
await refreshKeys();
editKey = null;
}
};
const handleDelete = async () => {
if (!deleteKey) {
return;
}
try {
await api.keyApi.deleteKey(deleteKey.id);
notificationController.show({
message: `Removed API Key: ${deleteKey.name}`,
type: NotificationType.Info
});
} catch (error) {
handleError(error, 'Unable to remove API Key');
} finally {
await refreshKeys();
deleteKey = null;
}
};
</script>
{#if newKey}
<APIKeyForm
title="New API Key"
submitText="Create"
apiKey={newKey}
on:submit={handleCreate}
on:cancel={() => (newKey = null)}
/>
{/if}
{#if secret}
<APIKeySecret {secret} on:done={() => (secret = '')} />
{/if}
{#if editKey}
<APIKeyForm
submitText="Save"
apiKey={editKey}
on:submit={handleUpdate}
on:cancel={() => (editKey = null)}
/>
{/if}
{#if deleteKey}
<DeleteConfirmDialogue
prompt="Are you sure you want to delete this API Key?"
on:confirm={() => handleDelete()}
on:cancel={() => (deleteKey = null)}
/>
{/if}
<section class="my-4">
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
<div class="flex justify-end mb-2">
<button
on:click={() => (newKey = { name: 'API Key' })}
class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>New API Key
</button>
</div>
{#if keys.length > 0}
<table class="text-left w-full">
<thead
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
>
<tr class="flex w-full place-items-center">
<th class="text-center w-1/3 font-medium text-sm">Name</th>
<th class="text-center w-1/3 font-medium text-sm">Created</th>
<th class="text-center w-1/3 font-medium text-sm">Action</th>
</tr>
</thead>
<tbody class="overflow-y-auto rounded-md w-full block border dark:border-immich-dark-gray">
{#each keys as key, i}
{#key key.id}
<tr
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
i % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td>
<td class="text-sm px-4 w-1/3 text-ellipsis"
>{new Date(key.createdAt).toLocaleDateString(locale, format)}
</td>
<td class="text-sm px-4 w-1/3 text-ellipsis">
<button
on:click={() => (editKey = key)}
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
>
<PencilOutline size="16" />
</button>
<button
on:click={() => (deleteKey = key)}
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
>
<TrashCanOutline size="16" />
</button>
</td>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
</section>

View File

@ -5,6 +5,7 @@
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
import ChangePasswordSettings from './change-password-settings.svelte';
import OAuthSettings from './oauth-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte';
export let user: UserResponseDto;
@ -32,6 +33,10 @@
<ChangePasswordSettings />
</SettingAccordion>
<SettingAccordion title="API Keys" subtitle="View and manage your API keys">
<UserAPIKeyList />
</SettingAccordion>
{#if oauthEnabled}
<SettingAccordion
title="OAuth"