mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: user pin-code (#18138)
* feat: user pincode * pr feedback * chore: cleanup --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
55af925ab3
commit
3f719bd8d7
14
i18n/en.json
14
i18n/en.json
@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"user_pin_code_settings": "PIN Code",
|
||||||
|
"user_pin_code_settings_description": "Manage your PIN code",
|
||||||
|
"current_pin_code": "Current PIN code",
|
||||||
|
"new_pin_code": "New PIN code",
|
||||||
|
"setup_pin_code": "Setup a PIN code",
|
||||||
|
"confirm_new_pin_code": "Confirm new PIN code",
|
||||||
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
|
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||||
|
"pin_code_changed_successfully": "Successfully changed PIN code",
|
||||||
|
"pin_code_setup_successfully": "Successfully setup a PIN code",
|
||||||
|
"pin_code_reset_successfully": "Successfully reset PIN code",
|
||||||
|
"reset_pin_code": "Reset PIN code",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"account_settings": "Account Settings",
|
"account_settings": "Account Settings",
|
||||||
@ -53,6 +65,7 @@
|
|||||||
"confirm_email_below": "To confirm, type \"{email}\" below",
|
"confirm_email_below": "To confirm, type \"{email}\" below",
|
||||||
"confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
|
"confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
|
||||||
"confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
|
"confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
|
||||||
|
"confirm_user_pin_code_reset": "Are you sure you want to reset {user}'s PIN code?",
|
||||||
"create_job": "Create job",
|
"create_job": "Create job",
|
||||||
"cron_expression": "Cron expression",
|
"cron_expression": "Cron expression",
|
||||||
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
|
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
|
||||||
@ -922,6 +935,7 @@
|
|||||||
"unable_to_remove_reaction": "Unable to remove reaction",
|
"unable_to_remove_reaction": "Unable to remove reaction",
|
||||||
"unable_to_repair_items": "Unable to repair items",
|
"unable_to_repair_items": "Unable to repair items",
|
||||||
"unable_to_reset_password": "Unable to reset password",
|
"unable_to_reset_password": "Unable to reset password",
|
||||||
|
"unable_to_reset_pin_code": "Unable to reset PIN code",
|
||||||
"unable_to_resolve_duplicate": "Unable to resolve duplicate",
|
"unable_to_resolve_duplicate": "Unable to resolve duplicate",
|
||||||
"unable_to_restore_assets": "Unable to restore assets",
|
"unable_to_restore_assets": "Unable to restore assets",
|
||||||
"unable_to_restore_trash": "Unable to restore trash",
|
"unable_to_restore_trash": "Unable to restore trash",
|
||||||
|
7
mobile/openapi/README.md
generated
7
mobile/openapi/README.md
generated
@ -109,8 +109,12 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |
|
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |
|
||||||
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |
|
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |
|
||||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||||
|
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code |
|
||||||
|
*AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status |
|
||||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||||
|
*AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code |
|
||||||
|
*AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code |
|
||||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||||
@ -304,6 +308,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
- [AssetVisibility](doc//AssetVisibility.md)
|
- [AssetVisibility](doc//AssetVisibility.md)
|
||||||
- [AudioCodec](doc//AudioCodec.md)
|
- [AudioCodec](doc//AudioCodec.md)
|
||||||
|
- [AuthStatusResponseDto](doc//AuthStatusResponseDto.md)
|
||||||
- [AvatarUpdate](doc//AvatarUpdate.md)
|
- [AvatarUpdate](doc//AvatarUpdate.md)
|
||||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||||
@ -383,6 +388,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||||
|
- [PinCodeChangeDto](doc//PinCodeChangeDto.md)
|
||||||
|
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
||||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||||
|
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@ -108,6 +108,7 @@ part 'model/asset_stats_response_dto.dart';
|
|||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
part 'model/asset_visibility.dart';
|
part 'model/asset_visibility.dart';
|
||||||
part 'model/audio_codec.dart';
|
part 'model/audio_codec.dart';
|
||||||
|
part 'model/auth_status_response_dto.dart';
|
||||||
part 'model/avatar_update.dart';
|
part 'model/avatar_update.dart';
|
||||||
part 'model/bulk_id_response_dto.dart';
|
part 'model/bulk_id_response_dto.dart';
|
||||||
part 'model/bulk_ids_dto.dart';
|
part 'model/bulk_ids_dto.dart';
|
||||||
@ -187,6 +188,8 @@ part 'model/person_response_dto.dart';
|
|||||||
part 'model/person_statistics_response_dto.dart';
|
part 'model/person_statistics_response_dto.dart';
|
||||||
part 'model/person_update_dto.dart';
|
part 'model/person_update_dto.dart';
|
||||||
part 'model/person_with_faces_response_dto.dart';
|
part 'model/person_with_faces_response_dto.dart';
|
||||||
|
part 'model/pin_code_change_dto.dart';
|
||||||
|
part 'model/pin_code_setup_dto.dart';
|
||||||
part 'model/places_response_dto.dart';
|
part 'model/places_response_dto.dart';
|
||||||
part 'model/purchase_response.dart';
|
part 'model/purchase_response.dart';
|
||||||
part 'model/purchase_update.dart';
|
part 'model/purchase_update.dart';
|
||||||
|
158
mobile/openapi/lib/api/authentication_api.dart
generated
158
mobile/openapi/lib/api/authentication_api.dart
generated
@ -63,6 +63,86 @@ class AuthenticationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /auth/pin-code' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||||
|
Future<Response> changePinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/auth/pin-code';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = pinCodeChangeDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||||
|
Future<void> changePinCode(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||||
|
final response = await changePinCodeWithHttpInfo(pinCodeChangeDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /auth/status' operation and returns the [Response].
|
||||||
|
Future<Response> getAuthStatusWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/auth/status';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AuthStatusResponseDto?> getAuthStatus() async {
|
||||||
|
final response = await getAuthStatusWithHttpInfo();
|
||||||
|
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), 'AuthStatusResponseDto',) as AuthStatusResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
@ -151,6 +231,84 @@ class AuthenticationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||||
|
Future<Response> resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/auth/pin-code';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = pinCodeChangeDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||||
|
Future<void> resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||||
|
final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /auth/pin-code' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeSetupDto] pinCodeSetupDto (required):
|
||||||
|
Future<Response> setupPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/auth/pin-code';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = pinCodeSetupDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [PinCodeSetupDto] pinCodeSetupDto (required):
|
||||||
|
Future<void> setupPinCode(PinCodeSetupDto pinCodeSetupDto,) async {
|
||||||
|
final response = await setupPinCodeWithHttpInfo(pinCodeSetupDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
|
/// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@ -272,6 +272,8 @@ class ApiClient {
|
|||||||
return AssetVisibilityTypeTransformer().decode(value);
|
return AssetVisibilityTypeTransformer().decode(value);
|
||||||
case 'AudioCodec':
|
case 'AudioCodec':
|
||||||
return AudioCodecTypeTransformer().decode(value);
|
return AudioCodecTypeTransformer().decode(value);
|
||||||
|
case 'AuthStatusResponseDto':
|
||||||
|
return AuthStatusResponseDto.fromJson(value);
|
||||||
case 'AvatarUpdate':
|
case 'AvatarUpdate':
|
||||||
return AvatarUpdate.fromJson(value);
|
return AvatarUpdate.fromJson(value);
|
||||||
case 'BulkIdResponseDto':
|
case 'BulkIdResponseDto':
|
||||||
@ -430,6 +432,10 @@ class ApiClient {
|
|||||||
return PersonUpdateDto.fromJson(value);
|
return PersonUpdateDto.fromJson(value);
|
||||||
case 'PersonWithFacesResponseDto':
|
case 'PersonWithFacesResponseDto':
|
||||||
return PersonWithFacesResponseDto.fromJson(value);
|
return PersonWithFacesResponseDto.fromJson(value);
|
||||||
|
case 'PinCodeChangeDto':
|
||||||
|
return PinCodeChangeDto.fromJson(value);
|
||||||
|
case 'PinCodeSetupDto':
|
||||||
|
return PinCodeSetupDto.fromJson(value);
|
||||||
case 'PlacesResponseDto':
|
case 'PlacesResponseDto':
|
||||||
return PlacesResponseDto.fromJson(value);
|
return PlacesResponseDto.fromJson(value);
|
||||||
case 'PurchaseResponse':
|
case 'PurchaseResponse':
|
||||||
|
107
mobile/openapi/lib/model/auth_status_response_dto.dart
generated
Normal file
107
mobile/openapi/lib/model/auth_status_response_dto.dart
generated
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// 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 AuthStatusResponseDto {
|
||||||
|
/// Returns a new [AuthStatusResponseDto] instance.
|
||||||
|
AuthStatusResponseDto({
|
||||||
|
required this.password,
|
||||||
|
required this.pinCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool password;
|
||||||
|
|
||||||
|
bool pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
|
||||||
|
other.password == password &&
|
||||||
|
other.pinCode == pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(password.hashCode) +
|
||||||
|
(pinCode.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AuthStatusResponseDto[password=$password, pinCode=$pinCode]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'password'] = this.password;
|
||||||
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [AuthStatusResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static AuthStatusResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "AuthStatusResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return AuthStatusResponseDto(
|
||||||
|
password: mapValueOfType<bool>(json, r'password')!,
|
||||||
|
pinCode: mapValueOfType<bool>(json, r'pinCode')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AuthStatusResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AuthStatusResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AuthStatusResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, AuthStatusResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AuthStatusResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AuthStatusResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of AuthStatusResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<AuthStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AuthStatusResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = AuthStatusResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'password',
|
||||||
|
'pinCode',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
133
mobile/openapi/lib/model/pin_code_change_dto.dart
generated
Normal file
133
mobile/openapi/lib/model/pin_code_change_dto.dart
generated
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// 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 PinCodeChangeDto {
|
||||||
|
/// Returns a new [PinCodeChangeDto] instance.
|
||||||
|
PinCodeChangeDto({
|
||||||
|
required this.newPinCode,
|
||||||
|
this.password,
|
||||||
|
this.pinCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
String newPinCode;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? password;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is PinCodeChangeDto &&
|
||||||
|
other.newPinCode == newPinCode &&
|
||||||
|
other.password == password &&
|
||||||
|
other.pinCode == pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(newPinCode.hashCode) +
|
||||||
|
(password == null ? 0 : password!.hashCode) +
|
||||||
|
(pinCode == null ? 0 : pinCode!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'PinCodeChangeDto[newPinCode=$newPinCode, password=$password, pinCode=$pinCode]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'newPinCode'] = this.newPinCode;
|
||||||
|
if (this.password != null) {
|
||||||
|
json[r'password'] = this.password;
|
||||||
|
} else {
|
||||||
|
// json[r'password'] = null;
|
||||||
|
}
|
||||||
|
if (this.pinCode != null) {
|
||||||
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
} else {
|
||||||
|
// json[r'pinCode'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [PinCodeChangeDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static PinCodeChangeDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "PinCodeChangeDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return PinCodeChangeDto(
|
||||||
|
newPinCode: mapValueOfType<String>(json, r'newPinCode')!,
|
||||||
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
pinCode: mapValueOfType<String>(json, r'pinCode'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PinCodeChangeDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <PinCodeChangeDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = PinCodeChangeDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, PinCodeChangeDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, PinCodeChangeDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = PinCodeChangeDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of PinCodeChangeDto-objects as value to a dart map
|
||||||
|
static Map<String, List<PinCodeChangeDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<PinCodeChangeDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = PinCodeChangeDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'newPinCode',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
99
mobile/openapi/lib/model/pin_code_setup_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/pin_code_setup_dto.dart
generated
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// 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 PinCodeSetupDto {
|
||||||
|
/// Returns a new [PinCodeSetupDto] instance.
|
||||||
|
PinCodeSetupDto({
|
||||||
|
required this.pinCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
String pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is PinCodeSetupDto &&
|
||||||
|
other.pinCode == pinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(pinCode.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'PinCodeSetupDto[pinCode=$pinCode]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [PinCodeSetupDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static PinCodeSetupDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "PinCodeSetupDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return PinCodeSetupDto(
|
||||||
|
pinCode: mapValueOfType<String>(json, r'pinCode')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PinCodeSetupDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <PinCodeSetupDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = PinCodeSetupDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, PinCodeSetupDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, PinCodeSetupDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = PinCodeSetupDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of PinCodeSetupDto-objects as value to a dart map
|
||||||
|
static Map<String, List<PinCodeSetupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<PinCodeSetupDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = PinCodeSetupDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'pinCode',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
@ -17,6 +17,7 @@ class UserAdminUpdateDto {
|
|||||||
this.email,
|
this.email,
|
||||||
this.name,
|
this.name,
|
||||||
this.password,
|
this.password,
|
||||||
|
this.pinCode,
|
||||||
this.quotaSizeInBytes,
|
this.quotaSizeInBytes,
|
||||||
this.shouldChangePassword,
|
this.shouldChangePassword,
|
||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
@ -48,6 +49,8 @@ class UserAdminUpdateDto {
|
|||||||
///
|
///
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
|
String? pinCode;
|
||||||
|
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
int? quotaSizeInBytes;
|
int? quotaSizeInBytes;
|
||||||
|
|
||||||
@ -67,6 +70,7 @@ class UserAdminUpdateDto {
|
|||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
|
other.pinCode == pinCode &&
|
||||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||||
other.shouldChangePassword == shouldChangePassword &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.storageLabel == storageLabel;
|
other.storageLabel == storageLabel;
|
||||||
@ -78,12 +82,13 @@ class UserAdminUpdateDto {
|
|||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
|
(pinCode == null ? 0 : pinCode!.hashCode) +
|
||||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||||
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
|
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
|
||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, pinCode=$pinCode, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -107,6 +112,11 @@ class UserAdminUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'password'] = null;
|
// json[r'password'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.pinCode != null) {
|
||||||
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
} else {
|
||||||
|
// json[r'pinCode'] = null;
|
||||||
|
}
|
||||||
if (this.quotaSizeInBytes != null) {
|
if (this.quotaSizeInBytes != null) {
|
||||||
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
||||||
} else {
|
} else {
|
||||||
@ -138,6 +148,7 @@ class UserAdminUpdateDto {
|
|||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
pinCode: mapValueOfType<String>(json, r'pinCode'),
|
||||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
|
||||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||||
|
@ -2294,6 +2294,139 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/pin-code": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "resetPinCode",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PinCodeChangeDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "setupPinCode",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PinCodeSetupDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "changePinCode",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PinCodeChangeDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/status": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAuthStatus",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AuthStatusResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/validateToken": {
|
"/auth/validateToken": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "validateAccessToken",
|
"operationId": "validateAccessToken",
|
||||||
@ -9031,6 +9164,21 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"AuthStatusResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"password",
|
||||||
|
"pinCode"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"AvatarUpdate": {
|
"AvatarUpdate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"color": {
|
"color": {
|
||||||
@ -10964,6 +11112,37 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PinCodeChangeDto": {
|
||||||
|
"properties": {
|
||||||
|
"newPinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"newPinCode"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PinCodeSetupDto": {
|
||||||
|
"properties": {
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"pinCode"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"PlacesResponseDto": {
|
"PlacesResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"admin1name": {
|
"admin1name": {
|
||||||
@ -13958,6 +14137,11 @@
|
|||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"quotaSizeInBytes": {
|
"quotaSizeInBytes": {
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
|
@ -123,6 +123,7 @@ export type UserAdminUpdateDto = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
pinCode?: string | null;
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
shouldChangePassword?: boolean;
|
shouldChangePassword?: boolean;
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
@ -510,6 +511,18 @@ export type LogoutResponseDto = {
|
|||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
successful: boolean;
|
successful: boolean;
|
||||||
};
|
};
|
||||||
|
export type PinCodeChangeDto = {
|
||||||
|
newPinCode: string;
|
||||||
|
password?: string;
|
||||||
|
pinCode?: string;
|
||||||
|
};
|
||||||
|
export type PinCodeSetupDto = {
|
||||||
|
pinCode: string;
|
||||||
|
};
|
||||||
|
export type AuthStatusResponseDto = {
|
||||||
|
password: boolean;
|
||||||
|
pinCode: boolean;
|
||||||
|
};
|
||||||
export type ValidateAccessTokenResponseDto = {
|
export type ValidateAccessTokenResponseDto = {
|
||||||
authStatus: boolean;
|
authStatus: boolean;
|
||||||
};
|
};
|
||||||
@ -2017,6 +2030,41 @@ export function logout(opts?: Oazapfts.RequestOpts) {
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function resetPinCode({ pinCodeChangeDto }: {
|
||||||
|
pinCodeChangeDto: PinCodeChangeDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "DELETE",
|
||||||
|
body: pinCodeChangeDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function setupPinCode({ pinCodeSetupDto }: {
|
||||||
|
pinCodeSetupDto: PinCodeSetupDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: pinCodeSetupDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function changePinCode({ pinCodeChangeDto }: {
|
||||||
|
pinCodeChangeDto: PinCodeChangeDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "PUT",
|
||||||
|
body: pinCodeChangeDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AuthStatusResponseDto;
|
||||||
|
}>("/auth/status", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function validateAccessToken(opts?: Oazapfts.RequestOpts) {
|
export function validateAccessToken(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -142,4 +142,50 @@ describe(AuthController.name, () => {
|
|||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /auth/pin-code', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '123456' });
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject 5 digits', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject 7 digits', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject non-numbers', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /auth/pin-code', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put('/auth/pin-code').send({ pinCode: '123456', newPinCode: '654321' });
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /auth/pin-code', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).delete('/auth/pin-code').send({ pinCode: '123456' });
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /auth/status', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get('/auth/status');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
|
AuthStatusResponseDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
|
PinCodeChangeDto,
|
||||||
|
PinCodeSetupDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
@ -74,4 +77,28 @@ export class AuthController {
|
|||||||
ImmichCookie.IS_AUTHENTICATED,
|
ImmichCookie.IS_AUTHENTICATED,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('status')
|
||||||
|
@Authenticated()
|
||||||
|
getAuthStatus(@Auth() auth: AuthDto): Promise<AuthStatusResponseDto> {
|
||||||
|
return this.service.getAuthStatus(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('pin-code')
|
||||||
|
@Authenticated()
|
||||||
|
setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> {
|
||||||
|
return this.service.setupPinCode(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('pin-code')
|
||||||
|
@Authenticated()
|
||||||
|
async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
|
||||||
|
return this.service.changePinCode(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('pin-code')
|
||||||
|
@Authenticated()
|
||||||
|
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
|
||||||
|
return this.service.resetPinCode(auth, dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Transform } from 'class-transformer';
|
|||||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
||||||
import { ImmichCookie } from 'src/enum';
|
import { ImmichCookie } from 'src/enum';
|
||||||
import { Optional, toEmail } from 'src/validation';
|
import { Optional, PinCode, toEmail } from 'src/validation';
|
||||||
|
|
||||||
export type CookieResponse = {
|
export type CookieResponse = {
|
||||||
isSecure: boolean;
|
isSecure: boolean;
|
||||||
@ -78,6 +78,26 @@ export class ChangePasswordDto {
|
|||||||
newPassword!: string;
|
newPassword!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PinCodeSetupDto {
|
||||||
|
@PinCode()
|
||||||
|
pinCode!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PinCodeResetDto {
|
||||||
|
@PinCode({ optional: true })
|
||||||
|
pinCode?: string;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PinCodeChangeDto extends PinCodeResetDto {
|
||||||
|
@PinCode()
|
||||||
|
newPinCode!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ValidateAccessTokenResponseDto {
|
export class ValidateAccessTokenResponseDto {
|
||||||
authStatus!: boolean;
|
authStatus!: boolean;
|
||||||
}
|
}
|
||||||
@ -114,3 +134,8 @@ export class OAuthConfigDto {
|
|||||||
export class OAuthAuthorizeResponseDto {
|
export class OAuthAuthorizeResponseDto {
|
||||||
url!: string;
|
url!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthStatusResponseDto {
|
||||||
|
pinCode!: boolean;
|
||||||
|
password!: boolean;
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from
|
|||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
import { Optional, PinCode, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||||
|
|
||||||
export class UserUpdateMeDto {
|
export class UserUpdateMeDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
@ -116,6 +116,9 @@ export class UserAdminUpdateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
|
@PinCode({ optional: true, nullable: true, emptyToNull: true })
|
||||||
|
pinCode?: string | null;
|
||||||
|
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ -87,6 +87,16 @@ where
|
|||||||
"users"."isAdmin" = $1
|
"users"."isAdmin" = $1
|
||||||
and "users"."deletedAt" is null
|
and "users"."deletedAt" is null
|
||||||
|
|
||||||
|
-- UserRepository.getForPinCode
|
||||||
|
select
|
||||||
|
"users"."pinCode",
|
||||||
|
"users"."password"
|
||||||
|
from
|
||||||
|
"users"
|
||||||
|
where
|
||||||
|
"users"."id" = $1
|
||||||
|
and "users"."deletedAt" is null
|
||||||
|
|
||||||
-- UserRepository.getByEmail
|
-- UserRepository.getByEmail
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
|
@ -89,13 +89,23 @@ export class UserRepository {
|
|||||||
return !!admin;
|
return !!admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForPinCode(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('users')
|
||||||
|
.select(['users.pinCode', 'users.password'])
|
||||||
|
.where('users.id', '=', id)
|
||||||
|
.where('users.deletedAt', 'is', null)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.EMAIL] })
|
@GenerateSql({ params: [DummyValue.EMAIL] })
|
||||||
getByEmail(email: string, withPassword?: boolean) {
|
getByEmail(email: string, options?: { withPassword?: boolean }) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(columns.userAdmin)
|
.select(columns.userAdmin)
|
||||||
.select(withMetadata)
|
.select(withMetadata)
|
||||||
.$if(!!withPassword, (eb) => eb.select('password'))
|
.$if(!!options?.withPassword, (eb) => eb.select('password'))
|
||||||
.where('email', '=', email)
|
.where('email', '=', email)
|
||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" ADD "pinCode" character varying;`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" DROP COLUMN "pinCode";`.execute(db);
|
||||||
|
}
|
@ -37,6 +37,9 @@ export class UserTable {
|
|||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
password!: Generated<string>;
|
password!: Generated<string>;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
pinCode!: string | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Generated<Timestamp>;
|
createdAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { UserAdmin } from 'src/database';
|
import { UserAdmin } from 'src/database';
|
||||||
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
||||||
import { AuthType, Permission } from 'src/enum';
|
import { AuthType, Permission } from 'src/enum';
|
||||||
@ -118,7 +119,7 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
await sut.changePassword(auth, dto);
|
await sut.changePassword(auth, dto);
|
||||||
|
|
||||||
expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
|
expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, { withPassword: true });
|
||||||
expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -859,4 +860,77 @@ describe(AuthService.name, () => {
|
|||||||
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
|
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setupPinCode', () => {
|
||||||
|
it('should setup a PIN code', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
const auth = factory.auth({ user });
|
||||||
|
const dto = { pinCode: '123456' };
|
||||||
|
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: null, password: '' });
|
||||||
|
mocks.user.update.mockResolvedValue(user);
|
||||||
|
|
||||||
|
await sut.setupPinCode(auth, dto);
|
||||||
|
|
||||||
|
expect(mocks.user.getForPinCode).toHaveBeenCalledWith(user.id);
|
||||||
|
expect(mocks.crypto.hashBcrypt).toHaveBeenCalledWith('123456', SALT_ROUNDS);
|
||||||
|
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: expect.any(String) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the user already has a PIN code', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
const auth = factory.auth({ user });
|
||||||
|
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
|
|
||||||
|
await expect(sut.setupPinCode(auth, { pinCode: '123456' })).rejects.toThrow('User already has a PIN code');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changePinCode', () => {
|
||||||
|
it('should change the PIN code', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
const auth = factory.auth({ user });
|
||||||
|
const dto = { pinCode: '123456', newPinCode: '012345' };
|
||||||
|
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
|
mocks.user.update.mockResolvedValue(user);
|
||||||
|
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||||
|
|
||||||
|
await sut.changePinCode(auth, dto);
|
||||||
|
|
||||||
|
expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('123456', '123456 (hashed)');
|
||||||
|
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: '012345 (hashed)' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the PIN code does not match', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
|
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.changePinCode(factory.auth({ user }), { pinCode: '000000', newPinCode: '012345' }),
|
||||||
|
).rejects.toThrow('Wrong PIN code');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetPinCode', () => {
|
||||||
|
it('should reset the PIN code', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
|
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||||
|
|
||||||
|
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
||||||
|
|
||||||
|
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the PIN code does not match', async () => {
|
||||||
|
const user = factory.userAdmin();
|
||||||
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
|
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||||
|
|
||||||
|
await expect(sut.resetPinCode(factory.auth({ user }), { pinCode: '000000' })).rejects.toThrow('Wrong PIN code');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,11 +9,15 @@ import { StorageCore } from 'src/cores/storage.core';
|
|||||||
import { UserAdmin } from 'src/database';
|
import { UserAdmin } from 'src/database';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
|
AuthStatusResponseDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
|
PinCodeChangeDto,
|
||||||
|
PinCodeResetDto,
|
||||||
|
PinCodeSetupDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
mapLoginResponse,
|
mapLoginResponse,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
@ -56,9 +60,9 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Password login has been disabled');
|
throw new UnauthorizedException('Password login has been disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await this.userRepository.getByEmail(dto.email, true);
|
let user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
|
||||||
if (user) {
|
if (user) {
|
||||||
const isAuthenticated = this.validatePassword(dto.password, user);
|
const isAuthenticated = this.validateSecret(dto.password, user.password);
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
user = undefined;
|
user = undefined;
|
||||||
}
|
}
|
||||||
@ -86,12 +90,12 @@ export class AuthService extends BaseService {
|
|||||||
|
|
||||||
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||||
const { password, newPassword } = dto;
|
const { password, newPassword } = dto;
|
||||||
const user = await this.userRepository.getByEmail(auth.user.email, true);
|
const user = await this.userRepository.getByEmail(auth.user.email, { withPassword: true });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = this.validatePassword(password, user);
|
const valid = this.validateSecret(password, user.password);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new BadRequestException('Wrong password');
|
throw new BadRequestException('Wrong password');
|
||||||
}
|
}
|
||||||
@ -103,6 +107,56 @@ export class AuthService extends BaseService {
|
|||||||
return mapUserAdmin(updatedUser);
|
return mapUserAdmin(updatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupPinCode(auth: AuthDto, { pinCode }: PinCodeSetupDto) {
|
||||||
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.pinCode) {
|
||||||
|
throw new BadRequestException('User already has a PIN code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashed = await this.cryptoRepository.hashBcrypt(pinCode, SALT_ROUNDS);
|
||||||
|
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
||||||
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
|
this.resetPinChecks(user, dto);
|
||||||
|
|
||||||
|
await this.userRepository.update(auth.user.id, { pinCode: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
||||||
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
|
this.resetPinChecks(user, dto);
|
||||||
|
|
||||||
|
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
||||||
|
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetPinChecks(
|
||||||
|
user: { pinCode: string | null; password: string | null },
|
||||||
|
dto: { pinCode?: string; password?: string },
|
||||||
|
) {
|
||||||
|
if (!user.pinCode) {
|
||||||
|
throw new BadRequestException('User does not have a PIN code');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.password) {
|
||||||
|
if (!this.validateSecret(dto.password, user.password)) {
|
||||||
|
throw new BadRequestException('Wrong password');
|
||||||
|
}
|
||||||
|
} else if (dto.pinCode) {
|
||||||
|
if (!this.validateSecret(dto.pinCode, user.pinCode)) {
|
||||||
|
throw new BadRequestException('Wrong PIN code');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException('Either password or pinCode is required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
|
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
|
||||||
const adminUser = await this.userRepository.getAdmin();
|
const adminUser = await this.userRepository.getAdmin();
|
||||||
if (adminUser) {
|
if (adminUser) {
|
||||||
@ -371,11 +425,12 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Invalid API key');
|
throw new UnauthorizedException('Invalid API key');
|
||||||
}
|
}
|
||||||
|
|
||||||
private validatePassword(inputPassword: string, user: { password?: string }): boolean {
|
private validateSecret(inputSecret: string, existingHash?: string | null): boolean {
|
||||||
if (!user || !user.password) {
|
if (!existingHash) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
|
|
||||||
|
return this.cryptoRepository.compareBcrypt(inputSecret, existingHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateSession(tokenValue: string): Promise<AuthDto> {
|
private async validateSession(tokenValue: string): Promise<AuthDto> {
|
||||||
@ -428,4 +483,16 @@ export class AuthService extends BaseService {
|
|||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAuthStatus(auth: AuthDto): Promise<AuthStatusResponseDto> {
|
||||||
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinCode: !!user.pinCode,
|
||||||
|
password: !!user.password,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,10 @@ export class UserAdminService extends BaseService {
|
|||||||
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.pinCode) {
|
||||||
|
dto.pinCode = await this.cryptoRepository.hashBcrypt(dto.pinCode, SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
if (dto.storageLabel === '') {
|
if (dto.storageLabel === '') {
|
||||||
dto.storageLabel = null;
|
dto.storageLabel = null;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
|
Matches,
|
||||||
Validate,
|
Validate,
|
||||||
ValidateBy,
|
ValidateBy,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
@ -70,6 +71,22 @@ export class UUIDParamDto {
|
|||||||
id!: string;
|
id!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PinCodeOptions = { optional?: boolean } & OptionalOptions;
|
||||||
|
export const PinCode = ({ optional, ...options }: PinCodeOptions = {}) => {
|
||||||
|
const decorators = [
|
||||||
|
IsString(),
|
||||||
|
IsNotEmpty(),
|
||||||
|
Matches(/^\d{6}$/, { message: ({ property }) => `${property} must be a 6-digit numeric string` }),
|
||||||
|
ApiProperty({ example: '123456' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (optional) {
|
||||||
|
decorators.push(Optional(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyDecorators(...decorators);
|
||||||
|
};
|
||||||
|
|
||||||
export interface OptionalOptions extends ValidationOptions {
|
export interface OptionalOptions extends ValidationOptions {
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
/** convert empty strings to null */
|
/** convert empty strings to null */
|
||||||
|
114
web/src/lib/components/user-settings-page/PinCodeInput.svelte
Normal file
114
web/src/lib/components/user-settings-page/PinCodeInput.svelte
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
pinLength?: number;
|
||||||
|
tabindexStart?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { label, value = $bindable(''), pinLength = 6, tabindexStart = 0 }: Props = $props();
|
||||||
|
|
||||||
|
let pinValues = $state(Array.from({ length: pinLength }).fill(''));
|
||||||
|
let pinCodeInputElements: HTMLInputElement[] = $state([]);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (value === '') {
|
||||||
|
pinValues = Array.from({ length: pinLength }).fill('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const focusNext = (index: number) => {
|
||||||
|
pinCodeInputElements[Math.min(index + 1, pinLength - 1)]?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusPrev = (index: number) => {
|
||||||
|
if (index > 0) {
|
||||||
|
pinCodeInputElements[index - 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (event: Event, index: number) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
let currentPinValue = target.value;
|
||||||
|
|
||||||
|
if (target.value.length > 1) {
|
||||||
|
currentPinValue = value.slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(Number(value))) {
|
||||||
|
pinValues[index] = '';
|
||||||
|
target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pinValues[index] = currentPinValue;
|
||||||
|
|
||||||
|
value = pinValues.join('').trim();
|
||||||
|
|
||||||
|
if (value && index < pinLength - 1) {
|
||||||
|
focusNext(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
const index = pinCodeInputElements.indexOf(target);
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Tab': {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Backspace': {
|
||||||
|
if (target.value === '' && index > 0) {
|
||||||
|
focusPrev(index);
|
||||||
|
pinValues[index - 1] = '';
|
||||||
|
} else if (target.value !== '') {
|
||||||
|
pinValues[index] = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'ArrowLeft': {
|
||||||
|
if (index > 0) {
|
||||||
|
focusPrev(index);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'ArrowRight': {
|
||||||
|
if (index < pinLength - 1) {
|
||||||
|
focusNext(index);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (Number.isNaN(Number(event.key))) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
{#if label}
|
||||||
|
<label class="text-xs text-dark" for={pinCodeInputElements[0]?.id}>{label.toUpperCase()}</label>
|
||||||
|
{/if}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each { length: pinLength } as _, index (index)}
|
||||||
|
<input
|
||||||
|
tabindex={tabindexStart + index}
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
maxlength="1"
|
||||||
|
bind:this={pinCodeInputElements[index]}
|
||||||
|
id="pin-code-{index}"
|
||||||
|
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 bg-transparent text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono"
|
||||||
|
bind:value={pinValues[index]}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
oninput={(event) => handleInput(event, index)}
|
||||||
|
aria-label={`PIN digit ${index + 1} of ${pinLength}${label ? ` for ${label}` : ''}`}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
116
web/src/lib/components/user-settings-page/PinCodeSettings.svelte
Normal file
116
web/src/lib/components/user-settings-page/PinCodeSettings.svelte
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { changePinCode, getAuthStatus, setupPinCode } from '@immich/sdk';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
let hasPinCode = $state(false);
|
||||||
|
let currentPinCode = $state('');
|
||||||
|
let newPinCode = $state('');
|
||||||
|
let confirmPinCode = $state('');
|
||||||
|
let isLoading = $state(false);
|
||||||
|
let canSubmit = $derived(
|
||||||
|
(hasPinCode ? currentPinCode.length === 6 : true) && confirmPinCode.length === 6 && newPinCode === confirmPinCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const authStatus = await getAuthStatus();
|
||||||
|
hasPinCode = authStatus.pinCode;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await (hasPinCode ? handleChange() : handleSetup());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetup = async () => {
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await setupPinCode({ pinCodeSetupDto: { pinCode: newPinCode } });
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: $t('pin_code_setup_successfully'),
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('unable_to_setup_pin_code'));
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
hasPinCode = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = async () => {
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await changePinCode({ pinCodeChangeDto: { pinCode: currentPinCode, newPinCode } });
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: $t('pin_code_changed_successfully'),
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('unable_to_change_pin_code'));
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
currentPinCode = '';
|
||||||
|
newPinCode = '';
|
||||||
|
confirmPinCode = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="my-4">
|
||||||
|
<div in:fade={{ duration: 200 }}>
|
||||||
|
<form autocomplete="off" onsubmit={handleSubmit} class="mt-6">
|
||||||
|
<div class="flex flex-col gap-6 place-items-center place-content-center">
|
||||||
|
{#if hasPinCode}
|
||||||
|
<p class="text-dark">Change PIN code</p>
|
||||||
|
<PinCodeInput label={$t('current_pin_code')} bind:value={currentPinCode} tabindexStart={1} pinLength={6} />
|
||||||
|
|
||||||
|
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={7} pinLength={6} />
|
||||||
|
|
||||||
|
<PinCodeInput
|
||||||
|
label={$t('confirm_new_pin_code')}
|
||||||
|
bind:value={confirmPinCode}
|
||||||
|
tabindexStart={13}
|
||||||
|
pinLength={6}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<p class="text-dark">{$t('setup_pin_code')}</p>
|
||||||
|
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={1} pinLength={6} />
|
||||||
|
|
||||||
|
<PinCodeInput
|
||||||
|
label={$t('confirm_new_pin_code')}
|
||||||
|
bind:value={confirmPinCode}
|
||||||
|
tabindexStart={7}
|
||||||
|
pinLength={6}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<Button shape="round" color="secondary" type="button" size="small" onclick={resetForm}>
|
||||||
|
{$t('clear')}
|
||||||
|
</Button>
|
||||||
|
<Button shape="round" type="submit" size="small" loading={isLoading} disabled={!canSubmit}>
|
||||||
|
{hasPinCode ? $t('save') : $t('create')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -1,24 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ChangePinCodeSettings from '$lib/components/user-settings-page/PinCodeSettings.svelte';
|
||||||
|
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
||||||
|
import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
|
||||||
|
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
||||||
|
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
|
||||||
|
import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
|
||||||
import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
|
import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { oauth } from '$lib/utils';
|
import { oauth } from '$lib/utils';
|
||||||
import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
|
import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
|
||||||
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
|
|
||||||
import SettingAccordion from '../shared-components/settings/setting-accordion.svelte';
|
|
||||||
import AppSettings from './app-settings.svelte';
|
|
||||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
|
||||||
import DeviceList from './device-list.svelte';
|
|
||||||
import OAuthSettings from './oauth-settings.svelte';
|
|
||||||
import PartnerSettings from './partner-settings.svelte';
|
|
||||||
import UserAPIKeyList from './user-api-key-list.svelte';
|
|
||||||
import UserProfileSettings from './user-profile-settings.svelte';
|
|
||||||
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
|
||||||
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
|
|
||||||
import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
|
|
||||||
import {
|
import {
|
||||||
mdiAccountGroupOutline,
|
mdiAccountGroupOutline,
|
||||||
mdiAccountOutline,
|
mdiAccountOutline,
|
||||||
@ -29,11 +21,21 @@
|
|||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiFeatureSearchOutline,
|
mdiFeatureSearchOutline,
|
||||||
mdiKeyOutline,
|
mdiKeyOutline,
|
||||||
|
mdiLockSmart,
|
||||||
mdiOnepassword,
|
mdiOnepassword,
|
||||||
mdiServerOutline,
|
mdiServerOutline,
|
||||||
mdiTwoFactorAuthentication,
|
mdiTwoFactorAuthentication,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
|
import { t } from 'svelte-i18n';
|
||||||
|
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
|
||||||
|
import SettingAccordion from '../shared-components/settings/setting-accordion.svelte';
|
||||||
|
import AppSettings from './app-settings.svelte';
|
||||||
|
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||||
|
import DeviceList from './device-list.svelte';
|
||||||
|
import OAuthSettings from './oauth-settings.svelte';
|
||||||
|
import PartnerSettings from './partner-settings.svelte';
|
||||||
|
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||||
|
import UserProfileSettings from './user-profile-settings.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
keys?: ApiKeyResponseDto[];
|
keys?: ApiKeyResponseDto[];
|
||||||
@ -135,6 +137,16 @@
|
|||||||
<PartnerSettings user={$user} />
|
<PartnerSettings user={$user} />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
<SettingAccordion
|
||||||
|
icon={mdiLockSmart}
|
||||||
|
key="user-pin-code-settings"
|
||||||
|
title={$t('user_pin_code_settings')}
|
||||||
|
subtitle={$t('user_pin_code_settings_description')}
|
||||||
|
autoScrollTo={true}
|
||||||
|
>
|
||||||
|
<ChangePinCodeSettings />
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
icon={mdiKeyOutline}
|
icon={mdiKeyOutline}
|
||||||
key="user-purchase-settings"
|
key="user-purchase-settings"
|
||||||
|
@ -6,14 +6,17 @@
|
|||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||||
import { mdiAccountEditOutline } from '@mdi/js';
|
import { mdiAccountEditOutline, mdiLockSmart, mdiOnepassword } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserAdminResponseDto;
|
user: UserAdminResponseDto;
|
||||||
canResetPassword?: boolean;
|
canResetPassword?: boolean;
|
||||||
onClose: (
|
onClose: (
|
||||||
data?: { action: 'update'; data: UserAdminResponseDto } | { action: 'resetPassword'; data: string },
|
data?:
|
||||||
|
| { action: 'update'; data: UserAdminResponseDto }
|
||||||
|
| { action: 'resetPassword'; data: string }
|
||||||
|
| { action: 'resetPinCode' },
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +79,24 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetUserPincode = async () => {
|
||||||
|
const isConfirmed = await modalManager.openDialog({
|
||||||
|
prompt: $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
||||||
|
|
||||||
|
onClose({ action: 'resetPinCode' });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// TODO move password reset server-side
|
// TODO move password reset server-side
|
||||||
function generatePassword(length: number = 16) {
|
function generatePassword(length: number = 16) {
|
||||||
let generatedPassword = '';
|
let generatedPassword = '';
|
||||||
@ -151,13 +172,34 @@
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div class="flex gap-3 w-full">
|
<div class="w-full">
|
||||||
{#if canResetPassword}
|
<div class="flex gap-3 w-full">
|
||||||
<Button shape="round" color="warning" variant="filled" fullWidth onclick={resetPassword}
|
{#if canResetPassword}
|
||||||
>{$t('reset_password')}</Button
|
<Button
|
||||||
|
shape="round"
|
||||||
|
color="warning"
|
||||||
|
variant="filled"
|
||||||
|
fullWidth
|
||||||
|
onclick={resetPassword}
|
||||||
|
leadingIcon={mdiOnepassword}
|
||||||
|
>
|
||||||
|
{$t('reset_password')}</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
color="warning"
|
||||||
|
variant="filled"
|
||||||
|
fullWidth
|
||||||
|
onclick={resetUserPincode}
|
||||||
|
leadingIcon={mdiLockSmart}>{$t('reset_pin_code')}</Button
|
||||||
>
|
>
|
||||||
{/if}
|
</div>
|
||||||
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
|
|
||||||
|
<div class="w-full mt-4">
|
||||||
|
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -74,6 +74,10 @@
|
|||||||
await refresh();
|
await refresh();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'resetPinCode': {
|
||||||
|
notificationController.show({ type: NotificationType.Info, message: $t('pin_code_reset_successfully') });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,7 +141,7 @@
|
|||||||
{#if !immichUser.deletedAt}
|
{#if !immichUser.deletedAt}
|
||||||
<IconButton
|
<IconButton
|
||||||
shape="round"
|
shape="round"
|
||||||
size="small"
|
size="medium"
|
||||||
icon={mdiPencilOutline}
|
icon={mdiPencilOutline}
|
||||||
title={$t('edit_user')}
|
title={$t('edit_user')}
|
||||||
onclick={() => handleEdit(immichUser)}
|
onclick={() => handleEdit(immichUser)}
|
||||||
@ -146,7 +150,7 @@
|
|||||||
{#if immichUser.id !== $user.id}
|
{#if immichUser.id !== $user.id}
|
||||||
<IconButton
|
<IconButton
|
||||||
shape="round"
|
shape="round"
|
||||||
size="small"
|
size="medium"
|
||||||
icon={mdiTrashCanOutline}
|
icon={mdiTrashCanOutline}
|
||||||
title={$t('delete_user')}
|
title={$t('delete_user')}
|
||||||
onclick={() => handleDelete(immichUser)}
|
onclick={() => handleDelete(immichUser)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user