feat: lock auth session (#18322)

This commit is contained in:
Jason Rasmussen 2025-05-15 18:08:31 -04:00 committed by GitHub
parent ecb66fdb2c
commit c1150fe7e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 765 additions and 123 deletions

View File

@ -1142,6 +1142,7 @@
"location_picker_latitude_hint": "Enter your latitude here", "location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here", "location_picker_longitude_hint": "Enter your longitude here",
"lock": "Lock",
"locked_folder": "Locked Folder", "locked_folder": "Locked Folder",
"log_out": "Log out", "log_out": "Log out",
"log_out_all_devices": "Log Out All Devices", "log_out_all_devices": "Log Out All Devices",

View File

@ -111,13 +111,14 @@ Class | Method | HTTP request | Description
*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* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code |
*AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status | *AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status |
*AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock |
*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* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code |
*AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /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* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*AuthenticationApi* | [**verifyPinCode**](doc//AuthenticationApi.md#verifypincode) | **POST** /auth/pin-code/verify |
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
@ -198,6 +199,7 @@ Class | Method | HTTP request | Description
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |
*SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets | *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links | *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links | *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
@ -392,6 +394,7 @@ Class | Method | HTTP request | Description
- [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md)
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
- [PinCodeChangeDto](doc//PinCodeChangeDto.md) - [PinCodeChangeDto](doc//PinCodeChangeDto.md)
- [PinCodeResetDto](doc//PinCodeResetDto.md)
- [PinCodeSetupDto](doc//PinCodeSetupDto.md) - [PinCodeSetupDto](doc//PinCodeSetupDto.md)
- [PlacesResponseDto](doc//PlacesResponseDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md)
- [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseResponse](doc//PurchaseResponse.md)
@ -424,6 +427,7 @@ Class | Method | HTTP request | Description
- [SessionCreateDto](doc//SessionCreateDto.md) - [SessionCreateDto](doc//SessionCreateDto.md)
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md)
- [SessionUnlockDto](doc//SessionUnlockDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
- [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md)
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)

View File

@ -189,6 +189,7 @@ 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_change_dto.dart';
part 'model/pin_code_reset_dto.dart';
part 'model/pin_code_setup_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';
@ -221,6 +222,7 @@ part 'model/server_version_response_dto.dart';
part 'model/session_create_dto.dart'; part 'model/session_create_dto.dart';
part 'model/session_create_response_dto.dart'; part 'model/session_create_response_dto.dart';
part 'model/session_response_dto.dart'; part 'model/session_response_dto.dart';
part 'model/session_unlock_dto.dart';
part 'model/shared_link_create_dto.dart'; part 'model/shared_link_create_dto.dart';
part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_edit_dto.dart';
part 'model/shared_link_response_dto.dart'; part 'model/shared_link_response_dto.dart';

View File

@ -143,6 +143,39 @@ class AuthenticationApi {
return null; return null;
} }
/// Performs an HTTP 'POST /auth/session/lock' operation and returns the [Response].
Future<Response> lockAuthSessionWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/auth/session/lock';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<void> lockAuthSession() async {
final response = await lockAuthSessionWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response]. /// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
@ -234,13 +267,13 @@ class AuthenticationApi {
/// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response]. /// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [PinCodeChangeDto] pinCodeChangeDto (required): /// * [PinCodeResetDto] pinCodeResetDto (required):
Future<Response> resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async { Future<Response> resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/auth/pin-code'; final apiPath = r'/auth/pin-code';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody = pinCodeChangeDto; Object? postBody = pinCodeResetDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
@ -262,9 +295,9 @@ class AuthenticationApi {
/// Parameters: /// Parameters:
/// ///
/// * [PinCodeChangeDto] pinCodeChangeDto (required): /// * [PinCodeResetDto] pinCodeResetDto (required):
Future<void> resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async { Future<void> resetPinCode(PinCodeResetDto pinCodeResetDto,) async {
final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,); final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@ -356,6 +389,45 @@ class AuthenticationApi {
return null; return null;
} }
/// Performs an HTTP 'POST /auth/session/unlock' operation and returns the [Response].
/// Parameters:
///
/// * [SessionUnlockDto] sessionUnlockDto (required):
Future<Response> unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/auth/session/unlock';
// ignore: prefer_final_locals
Object? postBody = sessionUnlockDto;
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:
///
/// * [SessionUnlockDto] sessionUnlockDto (required):
Future<void> unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async {
final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response]. /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
Future<Response> validateAccessTokenWithHttpInfo() async { Future<Response> validateAccessTokenWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
@ -396,43 +468,4 @@ class AuthenticationApi {
} }
return null; return null;
} }
/// Performs an HTTP 'POST /auth/pin-code/verify' operation and returns the [Response].
/// Parameters:
///
/// * [PinCodeSetupDto] pinCodeSetupDto (required):
Future<Response> verifyPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/auth/pin-code/verify';
// 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> verifyPinCode(PinCodeSetupDto pinCodeSetupDto,) async {
final response = await verifyPinCodeWithHttpInfo(pinCodeSetupDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
} }

View File

@ -179,4 +179,44 @@ class SessionsApi {
} }
return null; return null;
} }
/// Performs an HTTP 'POST /sessions/{id}/lock' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> lockSessionWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/sessions/{id}/lock'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> lockSession(String id,) async {
final response = await lockSessionWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
} }

View File

@ -434,6 +434,8 @@ class ApiClient {
return PersonWithFacesResponseDto.fromJson(value); return PersonWithFacesResponseDto.fromJson(value);
case 'PinCodeChangeDto': case 'PinCodeChangeDto':
return PinCodeChangeDto.fromJson(value); return PinCodeChangeDto.fromJson(value);
case 'PinCodeResetDto':
return PinCodeResetDto.fromJson(value);
case 'PinCodeSetupDto': case 'PinCodeSetupDto':
return PinCodeSetupDto.fromJson(value); return PinCodeSetupDto.fromJson(value);
case 'PlacesResponseDto': case 'PlacesResponseDto':
@ -498,6 +500,8 @@ class ApiClient {
return SessionCreateResponseDto.fromJson(value); return SessionCreateResponseDto.fromJson(value);
case 'SessionResponseDto': case 'SessionResponseDto':
return SessionResponseDto.fromJson(value); return SessionResponseDto.fromJson(value);
case 'SessionUnlockDto':
return SessionUnlockDto.fromJson(value);
case 'SharedLinkCreateDto': case 'SharedLinkCreateDto':
return SharedLinkCreateDto.fromJson(value); return SharedLinkCreateDto.fromJson(value);
case 'SharedLinkEditDto': case 'SharedLinkEditDto':

View File

@ -13,38 +13,70 @@ part of openapi.api;
class AuthStatusResponseDto { class AuthStatusResponseDto {
/// Returns a new [AuthStatusResponseDto] instance. /// Returns a new [AuthStatusResponseDto] instance.
AuthStatusResponseDto({ AuthStatusResponseDto({
this.expiresAt,
required this.isElevated, required this.isElevated,
required this.password, required this.password,
required this.pinCode, required this.pinCode,
this.pinExpiresAt,
}); });
///
/// 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? expiresAt;
bool isElevated; bool isElevated;
bool password; bool password;
bool pinCode; bool pinCode;
///
/// 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? pinExpiresAt;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto && bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
other.expiresAt == expiresAt &&
other.isElevated == isElevated && other.isElevated == isElevated &&
other.password == password && other.password == password &&
other.pinCode == pinCode; other.pinCode == pinCode &&
other.pinExpiresAt == pinExpiresAt;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(isElevated.hashCode) + (isElevated.hashCode) +
(password.hashCode) + (password.hashCode) +
(pinCode.hashCode); (pinCode.hashCode) +
(pinExpiresAt == null ? 0 : pinExpiresAt!.hashCode);
@override @override
String toString() => 'AuthStatusResponseDto[isElevated=$isElevated, password=$password, pinCode=$pinCode]'; String toString() => 'AuthStatusResponseDto[expiresAt=$expiresAt, isElevated=$isElevated, password=$password, pinCode=$pinCode, pinExpiresAt=$pinExpiresAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.expiresAt != null) {
json[r'expiresAt'] = this.expiresAt;
} else {
// json[r'expiresAt'] = null;
}
json[r'isElevated'] = this.isElevated; json[r'isElevated'] = this.isElevated;
json[r'password'] = this.password; json[r'password'] = this.password;
json[r'pinCode'] = this.pinCode; json[r'pinCode'] = this.pinCode;
if (this.pinExpiresAt != null) {
json[r'pinExpiresAt'] = this.pinExpiresAt;
} else {
// json[r'pinExpiresAt'] = null;
}
return json; return json;
} }
@ -57,9 +89,11 @@ class AuthStatusResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AuthStatusResponseDto( return AuthStatusResponseDto(
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
isElevated: mapValueOfType<bool>(json, r'isElevated')!, isElevated: mapValueOfType<bool>(json, r'isElevated')!,
password: mapValueOfType<bool>(json, r'password')!, password: mapValueOfType<bool>(json, r'password')!,
pinCode: mapValueOfType<bool>(json, r'pinCode')!, pinCode: mapValueOfType<bool>(json, r'pinCode')!,
pinExpiresAt: mapValueOfType<String>(json, r'pinExpiresAt'),
); );
} }
return null; return null;

View File

@ -85,6 +85,7 @@ class Permission {
static const sessionPeriodRead = Permission._(r'session.read'); static const sessionPeriodRead = Permission._(r'session.read');
static const sessionPeriodUpdate = Permission._(r'session.update'); static const sessionPeriodUpdate = Permission._(r'session.update');
static const sessionPeriodDelete = Permission._(r'session.delete'); static const sessionPeriodDelete = Permission._(r'session.delete');
static const sessionPeriodLock = Permission._(r'session.lock');
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create'); static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
@ -171,6 +172,7 @@ class Permission {
sessionPeriodRead, sessionPeriodRead,
sessionPeriodUpdate, sessionPeriodUpdate,
sessionPeriodDelete, sessionPeriodDelete,
sessionPeriodLock,
sharedLinkPeriodCreate, sharedLinkPeriodCreate,
sharedLinkPeriodRead, sharedLinkPeriodRead,
sharedLinkPeriodUpdate, sharedLinkPeriodUpdate,
@ -292,6 +294,7 @@ class PermissionTypeTransformer {
case r'session.read': return Permission.sessionPeriodRead; case r'session.read': return Permission.sessionPeriodRead;
case r'session.update': return Permission.sessionPeriodUpdate; case r'session.update': return Permission.sessionPeriodUpdate;
case r'session.delete': return Permission.sessionPeriodDelete; case r'session.delete': return Permission.sessionPeriodDelete;
case r'session.lock': return Permission.sessionPeriodLock;
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate; case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
case r'sharedLink.read': return Permission.sharedLinkPeriodRead; case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;

View File

@ -0,0 +1,125 @@
//
// 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 PinCodeResetDto {
/// Returns a new [PinCodeResetDto] instance.
PinCodeResetDto({
this.password,
this.pinCode,
});
///
/// 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 PinCodeResetDto &&
other.password == password &&
other.pinCode == pinCode;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(password == null ? 0 : password!.hashCode) +
(pinCode == null ? 0 : pinCode!.hashCode);
@override
String toString() => 'PinCodeResetDto[password=$password, pinCode=$pinCode]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
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 [PinCodeResetDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PinCodeResetDto? fromJson(dynamic value) {
upgradeDto(value, "PinCodeResetDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PinCodeResetDto(
password: mapValueOfType<String>(json, r'password'),
pinCode: mapValueOfType<String>(json, r'pinCode'),
);
}
return null;
}
static List<PinCodeResetDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PinCodeResetDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PinCodeResetDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PinCodeResetDto> mapFromJson(dynamic json) {
final map = <String, PinCodeResetDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PinCodeResetDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PinCodeResetDto-objects as value to a dart map
static Map<String, List<PinCodeResetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PinCodeResetDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PinCodeResetDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@ -17,6 +17,7 @@ class SessionCreateResponseDto {
required this.current, required this.current,
required this.deviceOS, required this.deviceOS,
required this.deviceType, required this.deviceType,
this.expiresAt,
required this.id, required this.id,
required this.token, required this.token,
required this.updatedAt, required this.updatedAt,
@ -30,6 +31,14 @@ class SessionCreateResponseDto {
String deviceType; String deviceType;
///
/// 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? expiresAt;
String id; String id;
String token; String token;
@ -42,6 +51,7 @@ class SessionCreateResponseDto {
other.current == current && other.current == current &&
other.deviceOS == deviceOS && other.deviceOS == deviceOS &&
other.deviceType == deviceType && other.deviceType == deviceType &&
other.expiresAt == expiresAt &&
other.id == id && other.id == id &&
other.token == token && other.token == token &&
other.updatedAt == updatedAt; other.updatedAt == updatedAt;
@ -53,12 +63,13 @@ class SessionCreateResponseDto {
(current.hashCode) + (current.hashCode) +
(deviceOS.hashCode) + (deviceOS.hashCode) +
(deviceType.hashCode) + (deviceType.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(token.hashCode) + (token.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, token=$token, updatedAt=$updatedAt]'; String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -66,6 +77,11 @@ class SessionCreateResponseDto {
json[r'current'] = this.current; json[r'current'] = this.current;
json[r'deviceOS'] = this.deviceOS; json[r'deviceOS'] = this.deviceOS;
json[r'deviceType'] = this.deviceType; json[r'deviceType'] = this.deviceType;
if (this.expiresAt != null) {
json[r'expiresAt'] = this.expiresAt;
} else {
// json[r'expiresAt'] = null;
}
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'token'] = this.token; json[r'token'] = this.token;
json[r'updatedAt'] = this.updatedAt; json[r'updatedAt'] = this.updatedAt;
@ -85,6 +101,7 @@ class SessionCreateResponseDto {
current: mapValueOfType<bool>(json, r'current')!, current: mapValueOfType<bool>(json, r'current')!,
deviceOS: mapValueOfType<String>(json, r'deviceOS')!, deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
deviceType: mapValueOfType<String>(json, r'deviceType')!, deviceType: mapValueOfType<String>(json, r'deviceType')!,
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
token: mapValueOfType<String>(json, r'token')!, token: mapValueOfType<String>(json, r'token')!,
updatedAt: mapValueOfType<String>(json, r'updatedAt')!, updatedAt: mapValueOfType<String>(json, r'updatedAt')!,

View File

@ -17,6 +17,7 @@ class SessionResponseDto {
required this.current, required this.current,
required this.deviceOS, required this.deviceOS,
required this.deviceType, required this.deviceType,
this.expiresAt,
required this.id, required this.id,
required this.updatedAt, required this.updatedAt,
}); });
@ -29,6 +30,14 @@ class SessionResponseDto {
String deviceType; String deviceType;
///
/// 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? expiresAt;
String id; String id;
String updatedAt; String updatedAt;
@ -39,6 +48,7 @@ class SessionResponseDto {
other.current == current && other.current == current &&
other.deviceOS == deviceOS && other.deviceOS == deviceOS &&
other.deviceType == deviceType && other.deviceType == deviceType &&
other.expiresAt == expiresAt &&
other.id == id && other.id == id &&
other.updatedAt == updatedAt; other.updatedAt == updatedAt;
@ -49,11 +59,12 @@ class SessionResponseDto {
(current.hashCode) + (current.hashCode) +
(deviceOS.hashCode) + (deviceOS.hashCode) +
(deviceType.hashCode) + (deviceType.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -61,6 +72,11 @@ class SessionResponseDto {
json[r'current'] = this.current; json[r'current'] = this.current;
json[r'deviceOS'] = this.deviceOS; json[r'deviceOS'] = this.deviceOS;
json[r'deviceType'] = this.deviceType; json[r'deviceType'] = this.deviceType;
if (this.expiresAt != null) {
json[r'expiresAt'] = this.expiresAt;
} else {
// json[r'expiresAt'] = null;
}
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'updatedAt'] = this.updatedAt; json[r'updatedAt'] = this.updatedAt;
return json; return json;
@ -79,6 +95,7 @@ class SessionResponseDto {
current: mapValueOfType<bool>(json, r'current')!, current: mapValueOfType<bool>(json, r'current')!,
deviceOS: mapValueOfType<String>(json, r'deviceOS')!, deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
deviceType: mapValueOfType<String>(json, r'deviceType')!, deviceType: mapValueOfType<String>(json, r'deviceType')!,
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
updatedAt: mapValueOfType<String>(json, r'updatedAt')!, updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
); );

View File

@ -0,0 +1,125 @@
//
// 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 SessionUnlockDto {
/// Returns a new [SessionUnlockDto] instance.
SessionUnlockDto({
this.password,
this.pinCode,
});
///
/// 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 SessionUnlockDto &&
other.password == password &&
other.pinCode == pinCode;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(password == null ? 0 : password!.hashCode) +
(pinCode == null ? 0 : pinCode!.hashCode);
@override
String toString() => 'SessionUnlockDto[password=$password, pinCode=$pinCode]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
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 [SessionUnlockDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SessionUnlockDto? fromJson(dynamic value) {
upgradeDto(value, "SessionUnlockDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SessionUnlockDto(
password: mapValueOfType<String>(json, r'password'),
pinCode: mapValueOfType<String>(json, r'pinCode'),
);
}
return null;
}
static List<SessionUnlockDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SessionUnlockDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SessionUnlockDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SessionUnlockDto> mapFromJson(dynamic json) {
final map = <String, SessionUnlockDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SessionUnlockDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SessionUnlockDto-objects as value to a dart map
static Map<String, List<SessionUnlockDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SessionUnlockDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SessionUnlockDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@ -2377,7 +2377,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/PinCodeChangeDto" "$ref": "#/components/schemas/PinCodeResetDto"
} }
} }
}, },
@ -2470,15 +2470,40 @@
] ]
} }
}, },
"/auth/pin-code/verify": { "/auth/session/lock": {
"post": { "post": {
"operationId": "verifyPinCode", "operationId": "lockAuthSession",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Authentication"
]
}
},
"/auth/session/unlock": {
"post": {
"operationId": "unlockAuthSession",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/PinCodeSetupDto" "$ref": "#/components/schemas/SessionUnlockDto"
} }
} }
}, },
@ -5695,6 +5720,41 @@
] ]
} }
}, },
"/sessions/{id}/lock": {
"post": {
"operationId": "lockSession",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sessions"
]
}
},
"/shared-links": { "/shared-links": {
"get": { "get": {
"operationId": "getAllSharedLinks", "operationId": "getAllSharedLinks",
@ -9327,6 +9387,9 @@
}, },
"AuthStatusResponseDto": { "AuthStatusResponseDto": {
"properties": { "properties": {
"expiresAt": {
"type": "string"
},
"isElevated": { "isElevated": {
"type": "boolean" "type": "boolean"
}, },
@ -9335,6 +9398,9 @@
}, },
"pinCode": { "pinCode": {
"type": "boolean" "type": "boolean"
},
"pinExpiresAt": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -11096,6 +11162,7 @@
"session.read", "session.read",
"session.update", "session.update",
"session.delete", "session.delete",
"session.lock",
"sharedLink.create", "sharedLink.create",
"sharedLink.read", "sharedLink.read",
"sharedLink.update", "sharedLink.update",
@ -11297,6 +11364,18 @@
], ],
"type": "object" "type": "object"
}, },
"PinCodeResetDto": {
"properties": {
"password": {
"type": "string"
},
"pinCode": {
"example": "123456",
"type": "string"
}
},
"type": "object"
},
"PinCodeSetupDto": { "PinCodeSetupDto": {
"properties": { "properties": {
"pinCode": { "pinCode": {
@ -12109,6 +12188,9 @@
"deviceType": { "deviceType": {
"type": "string" "type": "string"
}, },
"expiresAt": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -12144,6 +12226,9 @@
"deviceType": { "deviceType": {
"type": "string" "type": "string"
}, },
"expiresAt": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -12161,6 +12246,18 @@
], ],
"type": "object" "type": "object"
}, },
"SessionUnlockDto": {
"properties": {
"password": {
"type": "string"
},
"pinCode": {
"example": "123456",
"type": "string"
}
},
"type": "object"
},
"SharedLinkCreateDto": { "SharedLinkCreateDto": {
"properties": { "properties": {
"albumId": { "albumId": {

View File

@ -512,18 +512,28 @@ export type LogoutResponseDto = {
redirectUri: string; redirectUri: string;
successful: boolean; successful: boolean;
}; };
export type PinCodeChangeDto = { export type PinCodeResetDto = {
newPinCode: string;
password?: string; password?: string;
pinCode?: string; pinCode?: string;
}; };
export type PinCodeSetupDto = { export type PinCodeSetupDto = {
pinCode: string; pinCode: string;
}; };
export type PinCodeChangeDto = {
newPinCode: string;
password?: string;
pinCode?: string;
};
export type SessionUnlockDto = {
password?: string;
pinCode?: string;
};
export type AuthStatusResponseDto = { export type AuthStatusResponseDto = {
expiresAt?: string;
isElevated: boolean; isElevated: boolean;
password: boolean; password: boolean;
pinCode: boolean; pinCode: boolean;
pinExpiresAt?: string;
}; };
export type ValidateAccessTokenResponseDto = { export type ValidateAccessTokenResponseDto = {
authStatus: boolean; authStatus: boolean;
@ -1075,6 +1085,7 @@ export type SessionResponseDto = {
current: boolean; current: boolean;
deviceOS: string; deviceOS: string;
deviceType: string; deviceType: string;
expiresAt?: string;
id: string; id: string;
updatedAt: string; updatedAt: string;
}; };
@ -1089,6 +1100,7 @@ export type SessionCreateResponseDto = {
current: boolean; current: boolean;
deviceOS: string; deviceOS: string;
deviceType: string; deviceType: string;
expiresAt?: string;
id: string; id: string;
token: string; token: string;
updatedAt: string; updatedAt: string;
@ -2066,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) {
method: "POST" method: "POST"
})); }));
} }
export function resetPinCode({ pinCodeChangeDto }: { export function resetPinCode({ pinCodeResetDto }: {
pinCodeChangeDto: PinCodeChangeDto; pinCodeResetDto: PinCodeResetDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({ return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
...opts, ...opts,
method: "DELETE", method: "DELETE",
body: pinCodeChangeDto body: pinCodeResetDto
}))); })));
} }
export function setupPinCode({ pinCodeSetupDto }: { export function setupPinCode({ pinCodeSetupDto }: {
@ -2093,13 +2105,19 @@ export function changePinCode({ pinCodeChangeDto }: {
body: pinCodeChangeDto body: pinCodeChangeDto
}))); })));
} }
export function verifyPinCode({ pinCodeSetupDto }: { export function lockAuthSession(opts?: Oazapfts.RequestOpts) {
pinCodeSetupDto: PinCodeSetupDto; return oazapfts.ok(oazapfts.fetchText("/auth/session/lock", {
...opts,
method: "POST"
}));
}
export function unlockAuthSession({ sessionUnlockDto }: {
sessionUnlockDto: SessionUnlockDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code/verify", oazapfts.json({ return oazapfts.ok(oazapfts.fetchText("/auth/session/unlock", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
body: pinCodeSetupDto body: sessionUnlockDto
}))); })));
} }
export function getAuthStatus(opts?: Oazapfts.RequestOpts) { export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
@ -2952,6 +2970,14 @@ export function deleteSession({ id }: {
method: "DELETE" method: "DELETE"
})); }));
} }
export function lockSession({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}/lock`, {
...opts,
method: "POST"
}));
}
export function getAllSharedLinks({ albumId }: { export function getAllSharedLinks({ albumId }: {
albumId?: string; albumId?: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@ -3709,6 +3735,7 @@ export enum Permission {
SessionRead = "session.read", SessionRead = "session.read",
SessionUpdate = "session.update", SessionUpdate = "session.update",
SessionDelete = "session.delete", SessionDelete = "session.delete",
SessionLock = "session.lock",
SharedLinkCreate = "sharedLink.create", SharedLinkCreate = "sharedLink.create",
SharedLinkRead = "sharedLink.read", SharedLinkRead = "sharedLink.read",
SharedLinkUpdate = "sharedLink.update", SharedLinkUpdate = "sharedLink.update",

View File

@ -9,7 +9,9 @@ import {
LoginResponseDto, LoginResponseDto,
LogoutResponseDto, LogoutResponseDto,
PinCodeChangeDto, PinCodeChangeDto,
PinCodeResetDto,
PinCodeSetupDto, PinCodeSetupDto,
SessionUnlockDto,
SignUpDto, SignUpDto,
ValidateAccessTokenResponseDto, ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
@ -98,14 +100,21 @@ export class AuthController {
@Delete('pin-code') @Delete('pin-code')
@Authenticated() @Authenticated()
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> { async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
return this.service.resetPinCode(auth, dto); return this.service.resetPinCode(auth, dto);
} }
@Post('pin-code/verify') @Post('session/unlock')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated() @Authenticated()
async verifyPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> { async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
return this.service.verifyPinCode(auth, dto); return this.service.unlockSession(auth, dto);
}
@Post('session/lock')
@HttpCode(HttpStatus.OK)
@Authenticated()
async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
return this.service.lockSession(auth);
} }
} }

View File

@ -37,4 +37,11 @@ export class SessionController {
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@Post(':id/lock')
@Authenticated({ permission: Permission.SESSION_LOCK })
@HttpCode(HttpStatus.NO_CONTENT)
lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.lock(auth, id);
}
} }

View File

@ -232,6 +232,7 @@ export type Session = {
id: string; id: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
expiresAt: Date | null;
deviceOS: string; deviceOS: string;
deviceType: string; deviceType: string;
pinExpiresAt: Date | null; pinExpiresAt: Date | null;

2
server/src/db.d.ts vendored
View File

@ -344,7 +344,7 @@ export interface Sessions {
deviceType: Generated<string>; deviceType: Generated<string>;
id: Generated<string>; id: Generated<string>;
parentId: string | null; parentId: string | null;
expiredAt: Date | null; expiresAt: Date | null;
token: string; token: string;
updatedAt: Generated<Timestamp>; updatedAt: Generated<Timestamp>;
updateId: Generated<string>; updateId: Generated<string>;

View File

@ -93,6 +93,8 @@ export class PinCodeResetDto {
password?: string; password?: string;
} }
export class SessionUnlockDto extends PinCodeResetDto {}
export class PinCodeChangeDto extends PinCodeResetDto { export class PinCodeChangeDto extends PinCodeResetDto {
@PinCode() @PinCode()
newPinCode!: string; newPinCode!: string;
@ -139,4 +141,6 @@ export class AuthStatusResponseDto {
pinCode!: boolean; pinCode!: boolean;
password!: boolean; password!: boolean;
isElevated!: boolean; isElevated!: boolean;
expiresAt?: string;
pinExpiresAt?: string;
} }

View File

@ -24,6 +24,7 @@ export class SessionResponseDto {
id!: string; id!: string;
createdAt!: string; createdAt!: string;
updatedAt!: string; updatedAt!: string;
expiresAt?: string;
current!: boolean; current!: boolean;
deviceType!: string; deviceType!: string;
deviceOS!: string; deviceOS!: string;
@ -37,6 +38,7 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
id: entity.id, id: entity.id,
createdAt: entity.createdAt.toISOString(), createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(), updatedAt: entity.updatedAt.toISOString(),
expiresAt: entity.expiresAt?.toISOString(),
current: currentId === entity.id, current: currentId === entity.id,
deviceOS: entity.deviceOS, deviceOS: entity.deviceOS,
deviceType: entity.deviceType, deviceType: entity.deviceType,

View File

@ -148,6 +148,7 @@ export enum Permission {
SESSION_READ = 'session.read', SESSION_READ = 'session.read',
SESSION_UPDATE = 'session.update', SESSION_UPDATE = 'session.update',
SESSION_DELETE = 'session.delete', SESSION_DELETE = 'session.delete',
SESSION_LOCK = 'session.lock',
SHARED_LINK_CREATE = 'sharedLink.create', SHARED_LINK_CREATE = 'sharedLink.create',
SHARED_LINK_READ = 'sharedLink.read', SHARED_LINK_READ = 'sharedLink.read',

View File

@ -199,6 +199,15 @@ where
"partners"."sharedById" in ($1) "partners"."sharedById" in ($1)
and "partners"."sharedWithId" = $2 and "partners"."sharedWithId" = $2
-- AccessRepository.session.checkOwnerAccess
select
"sessions"."id"
from
"sessions"
where
"sessions"."id" in ($1)
and "sessions"."userId" = $2
-- AccessRepository.stack.checkOwnerAccess -- AccessRepository.stack.checkOwnerAccess
select select
"stacks"."id" "stacks"."id"

View File

@ -1,12 +1,14 @@
-- NOTE: This file is auto generated by ./sql-generator -- NOTE: This file is auto generated by ./sql-generator
-- SessionRepository.search -- SessionRepository.get
select select
* "id",
"expiresAt",
"pinExpiresAt"
from from
"sessions" "sessions"
where where
"sessions"."updatedAt" <= $1 "id" = $1
-- SessionRepository.getByToken -- SessionRepository.getByToken
select select
@ -37,8 +39,8 @@ from
where where
"sessions"."token" = $1 "sessions"."token" = $1
and ( and (
"sessions"."expiredAt" is null "sessions"."expiresAt" is null
or "sessions"."expiredAt" > $2 or "sessions"."expiresAt" > $2
) )
-- SessionRepository.getByUserId -- SessionRepository.getByUserId
@ -50,6 +52,10 @@ from
and "users"."deletedAt" is null and "users"."deletedAt" is null
where where
"sessions"."userId" = $1 "sessions"."userId" = $1
and (
"sessions"."expiresAt" is null
or "sessions"."expiresAt" > $2
)
order by order by
"sessions"."updatedAt" desc, "sessions"."updatedAt" desc,
"sessions"."createdAt" desc "sessions"."createdAt" desc
@ -58,3 +64,10 @@ order by
delete from "sessions" delete from "sessions"
where where
"id" = $1::uuid "id" = $1::uuid
-- SessionRepository.lockAll
update "sessions"
set
"pinExpiresAt" = $1
where
"userId" = $2

View File

@ -306,6 +306,25 @@ class NotificationAccess {
} }
} }
class SessionAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, sessionIds: Set<string>) {
if (sessionIds.size === 0) {
return new Set<string>();
}
return this.db
.selectFrom('sessions')
.select('sessions.id')
.where('sessions.id', 'in', [...sessionIds])
.where('sessions.userId', '=', userId)
.execute()
.then((sessions) => new Set(sessions.map((session) => session.id)));
}
}
class StackAccess { class StackAccess {
constructor(private db: Kysely<DB>) {} constructor(private db: Kysely<DB>) {}
@ -456,6 +475,7 @@ export class AccessRepository {
notification: NotificationAccess; notification: NotificationAccess;
person: PersonAccess; person: PersonAccess;
partner: PartnerAccess; partner: PartnerAccess;
session: SessionAccess;
stack: StackAccess; stack: StackAccess;
tag: TagAccess; tag: TagAccess;
timeline: TimelineAccess; timeline: TimelineAccess;
@ -469,6 +489,7 @@ export class AccessRepository {
this.notification = new NotificationAccess(db); this.notification = new NotificationAccess(db);
this.person = new PersonAccess(db); this.person = new PersonAccess(db);
this.partner = new PartnerAccess(db); this.partner = new PartnerAccess(db);
this.session = new SessionAccess(db);
this.stack = new StackAccess(db); this.stack = new StackAccess(db);
this.tag = new TagAccess(db); this.tag = new TagAccess(db);
this.timeline = new TimelineAccess(db); this.timeline = new TimelineAccess(db);

View File

@ -20,20 +20,20 @@ export class SessionRepository {
.where((eb) => .where((eb) =>
eb.or([ eb.or([
eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()), eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
eb.and([eb('expiredAt', 'is not', null), eb('expiredAt', '<=', DateTime.now().toJSDate())]), eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]),
]), ]),
) )
.returning(['id', 'deviceOS', 'deviceType']) .returning(['id', 'deviceOS', 'deviceType'])
.execute(); .execute();
} }
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) @GenerateSql({ params: [DummyValue.UUID] })
search(options: SessionSearchOptions) { get(id: string) {
return this.db return this.db
.selectFrom('sessions') .selectFrom('sessions')
.selectAll() .select(['id', 'expiresAt', 'pinExpiresAt'])
.where('sessions.updatedAt', '<=', options.updatedBefore) .where('id', '=', id)
.execute(); .executeTakeFirst();
} }
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.STRING] })
@ -52,7 +52,7 @@ export class SessionRepository {
]) ])
.where('sessions.token', '=', token) .where('sessions.token', '=', token)
.where((eb) => .where((eb) =>
eb.or([eb('sessions.expiredAt', 'is', null), eb('sessions.expiredAt', '>', DateTime.now().toJSDate())]), eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
) )
.executeTakeFirst(); .executeTakeFirst();
} }
@ -64,6 +64,9 @@ export class SessionRepository {
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null)) .innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
.selectAll('sessions') .selectAll('sessions')
.where('sessions.userId', '=', userId) .where('sessions.userId', '=', userId)
.where((eb) =>
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
)
.orderBy('sessions.updatedAt', 'desc') .orderBy('sessions.updatedAt', 'desc')
.orderBy('sessions.createdAt', 'desc') .orderBy('sessions.createdAt', 'desc')
.execute(); .execute();
@ -86,4 +89,9 @@ export class SessionRepository {
async delete(id: string) { async delete(id: string) {
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
} }
@GenerateSql({ params: [DummyValue.UUID] })
async lockAll(userId: string) {
await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
}
} }

View File

@ -0,0 +1,9 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "sessions" RENAME "expiredAt" TO "expiresAt";`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "sessions" RENAME "expiresAt" TO "expiredAt";`.execute(db);
}

View File

@ -26,7 +26,7 @@ export class SessionTable {
updatedAt!: Date; updatedAt!: Date;
@Column({ type: 'timestamp with time zone', nullable: true }) @Column({ type: 'timestamp with time zone', nullable: true })
expiredAt!: Date | null; expiresAt!: Date | null;
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
userId!: string; userId!: string;

View File

@ -924,13 +924,13 @@ describe(AuthService.name, () => {
const user = factory.userAdmin(); const user = factory.userAdmin();
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
mocks.session.getByUserId.mockResolvedValue([currentSession]); mocks.session.lockAll.mockResolvedValue(void 0);
mocks.session.update.mockResolvedValue(currentSession); mocks.session.update.mockResolvedValue(currentSession);
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' }); await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null }); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null }); expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
}); });
it('should throw if the PIN code does not match', async () => { it('should throw if the PIN code does not match', async () => {

View File

@ -18,6 +18,7 @@ import {
PinCodeChangeDto, PinCodeChangeDto,
PinCodeResetDto, PinCodeResetDto,
PinCodeSetupDto, PinCodeSetupDto,
SessionUnlockDto,
SignUpDto, SignUpDto,
mapLoginResponse, mapLoginResponse,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
@ -123,24 +124,21 @@ export class AuthService extends BaseService {
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) { async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
const user = await this.userRepository.getForPinCode(auth.user.id); const user = await this.userRepository.getForPinCode(auth.user.id);
this.resetPinChecks(user, dto); this.validatePinCode(user, dto);
await this.userRepository.update(auth.user.id, { pinCode: null }); await this.userRepository.update(auth.user.id, { pinCode: null });
const sessions = await this.sessionRepository.getByUserId(auth.user.id); await this.sessionRepository.lockAll(auth.user.id);
for (const session of sessions) {
await this.sessionRepository.update(session.id, { pinExpiresAt: null });
}
} }
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) { async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
const user = await this.userRepository.getForPinCode(auth.user.id); const user = await this.userRepository.getForPinCode(auth.user.id);
this.resetPinChecks(user, dto); this.validatePinCode(user, dto);
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS); const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
await this.userRepository.update(auth.user.id, { pinCode: hashed }); await this.userRepository.update(auth.user.id, { pinCode: hashed });
} }
private resetPinChecks( private validatePinCode(
user: { pinCode: string | null; password: string | null }, user: { pinCode: string | null; password: string | null },
dto: { pinCode?: string; password?: string }, dto: { pinCode?: string; password?: string },
) { ) {
@ -474,23 +472,27 @@ export class AuthService extends BaseService {
throw new UnauthorizedException('Invalid user token'); throw new UnauthorizedException('Invalid user token');
} }
async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise<void> { async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
const user = await this.userRepository.getForPinCode(auth.user.id);
if (!user) {
throw new UnauthorizedException();
}
this.resetPinChecks(user, { pinCode: dto.pinCode });
if (!auth.session) { if (!auth.session) {
throw new BadRequestException('Session is missing'); throw new BadRequestException('This endpoint can only be used with a session token');
} }
const user = await this.userRepository.getForPinCode(auth.user.id);
this.validatePinCode(user, { pinCode: dto.pinCode });
await this.sessionRepository.update(auth.session.id, { await this.sessionRepository.update(auth.session.id, {
pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()), pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
}); });
} }
async lockSession(auth: AuthDto): Promise<void> {
if (!auth.session) {
throw new BadRequestException('This endpoint can only be used with a session token');
}
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
}
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
const token = this.cryptoRepository.randomBytesAsText(32); const token = this.cryptoRepository.randomBytesAsText(32);
const tokenHashed = this.cryptoRepository.hashSha256(token); const tokenHashed = this.cryptoRepository.hashSha256(token);
@ -526,10 +528,14 @@ export class AuthService extends BaseService {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
return { return {
pinCode: !!user.pinCode, pinCode: !!user.pinCode,
password: !!user.password, password: !!user.password,
isElevated: !!auth.session?.hasElevatedPermission, isElevated: !!auth.session?.hasElevatedPermission,
expiresAt: session?.expiresAt?.toISOString(),
pinExpiresAt: session?.pinExpiresAt?.toISOString(),
}; };
} }
} }

View File

@ -30,7 +30,7 @@ export class SessionService extends BaseService {
const session = await this.sessionRepository.create({ const session = await this.sessionRepository.create({
parentId: auth.session.id, parentId: auth.session.id,
userId: auth.user.id, userId: auth.user.id,
expiredAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
deviceType: dto.deviceType, deviceType: dto.deviceType,
deviceOS: dto.deviceOS, deviceOS: dto.deviceOS,
token: tokenHashed, token: tokenHashed,
@ -49,6 +49,11 @@ export class SessionService extends BaseService {
await this.sessionRepository.delete(id); await this.sessionRepository.delete(id);
} }
async lock(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] });
await this.sessionRepository.update(id, { pinExpiresAt: null });
}
async deleteAll(auth: AuthDto): Promise<void> { async deleteAll(auth: AuthDto): Promise<void> {
const sessions = await this.sessionRepository.getByUserId(auth.user.id); const sessions = await this.sessionRepository.getByUserId(auth.user.id);
for (const session of sessions) { for (const session of sessions) {

View File

@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
return await access.partner.checkUpdateAccess(auth.user.id, ids); return await access.partner.checkUpdateAccess(auth.user.id, ids);
} }
case Permission.SESSION_READ:
case Permission.SESSION_UPDATE:
case Permission.SESSION_DELETE:
case Permission.SESSION_LOCK: {
return access.session.checkOwnerAccess(auth.user.id, ids);
}
case Permission.STACK_READ: { case Permission.STACK_READ: {
return access.stack.checkOwnerAccess(auth.user.id, ids); return access.stack.checkOwnerAccess(auth.user.id, ids);
} }

View File

@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()), checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
}, },
session: {
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
},
stack: { stack: {
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
}, },

View File

@ -127,7 +127,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
deviceType: 'mobile', deviceType: 'mobile',
token: 'abc123', token: 'abc123',
parentId: null, parentId: null,
expiredAt: null, expiresAt: null,
userId: newUuid(), userId: newUuid(),
pinExpiresAt: newDate(), pinExpiresAt: newDate(),
...session, ...session,

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
@ -10,11 +11,12 @@
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetStore } from '$lib/stores/assets-store.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility, lockAuthSession } from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js'; import { Button } from '@immich/ui';
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -42,6 +44,11 @@
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
assetStore.removeAssets(assetIds); assetStore.removeAssets(assetIds);
}; };
const handleLock = async () => {
await lockAuthSession();
await goto(AppRoute.PHOTOS);
};
</script> </script>
<!-- Multi-selection mode app bar --> <!-- Multi-selection mode app bar -->
@ -62,6 +69,12 @@
{/if} {/if}
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
{#snippet buttons()}
<Button size="small" variant="filled" color="warning" leadingIcon={mdiLockOutline} onclick={handleLock}>
{$t('lock')}
</Button>
{/snippet}
<AssetGrid <AssetGrid
enableRouting={true} enableRouting={true}
{assetStore} {assetStore}

View File

@ -8,14 +8,12 @@ import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => { export const load = (async ({ params, url }) => {
await authenticate(url); await authenticate(url);
const { isElevated, pinCode } = await getAuthStatus(); const { isElevated, pinCode } = await getAuthStatus();
if (!isElevated || !pinCode) { if (!isElevated || !pinCode) {
const continuePath = encodeURIComponent(url.pathname); redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
const redirectPath = `${AppRoute.AUTH_PIN_PROMPT}?continue=${continuePath}`;
redirect(302, redirectPath);
} }
const asset = await getAssetInfoFromParam(params); const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter(); const $t = await getFormatter();

View File

@ -3,9 +3,8 @@
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte'; import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte'; import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { verifyPinCode } from '@immich/sdk'; import { unlockAuthSession } from '@immich/sdk';
import { Icon } from '@immich/ui'; import { Icon } from '@immich/ui';
import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js'; import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -23,17 +22,15 @@
let hasPinCode = $derived(data.hasPinCode); let hasPinCode = $derived(data.hasPinCode);
let pinCode = $state(''); let pinCode = $state('');
const onPinFilled = async (code: string, withDelay = false) => { const handleUnlockSession = async (code: string) => {
try { try {
await verifyPinCode({ pinCodeSetupDto: { pinCode: code } }); await unlockAuthSession({ sessionUnlockDto: { pinCode: code } });
isVerified = true; isVerified = true;
if (withDelay) { await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
}
void goto(data.continuePath ?? AppRoute.LOCKED); await goto(data.continueUrl);
} catch (error) { } catch (error) {
handleError(error, $t('wrong_pin_code')); handleError(error, $t('wrong_pin_code'));
isBadPinCode = true; isBadPinCode = true;
@ -64,7 +61,7 @@
bind:value={pinCode} bind:value={pinCode}
tabindexStart={1} tabindexStart={1}
pinLength={6} pinLength={6}
onFilled={(pinCode) => onPinFilled(pinCode, true)} onFilled={handleUnlockSession}
/> />
</div> </div>
</div> </div>

View File

@ -1,3 +1,4 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getAuthStatus } from '@immich/sdk'; import { getAuthStatus } from '@immich/sdk';
@ -8,8 +9,6 @@ export const load = (async ({ url }) => {
const { pinCode } = await getAuthStatus(); const { pinCode } = await getAuthStatus();
const continuePath = url.searchParams.get('continue');
const $t = await getFormatter(); const $t = await getFormatter();
return { return {
@ -17,6 +16,6 @@ export const load = (async ({ url }) => {
title: $t('pin_verification'), title: $t('pin_verification'),
}, },
hasPinCode: !!pinCode, hasPinCode: !!pinCode,
continuePath, continueUrl: url.searchParams.get('continue') || AppRoute.LOCKED,
}; };
}) satisfies PageLoad; }) satisfies PageLoad;