diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e8cfda0b04..b1714e2764 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -3038,6 +3038,12 @@ export interface SharedLinkCreateDto { * @memberof SharedLinkCreateDto */ 'expiresAt'?: string | null; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'password'?: string; /** * * @type {boolean} @@ -3089,6 +3095,12 @@ export interface SharedLinkEditDto { * @memberof SharedLinkEditDto */ 'expiresAt'?: string | null; + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'password'?: string; /** * * @type {boolean} @@ -3156,12 +3168,24 @@ export interface SharedLinkResponseDto { * @memberof SharedLinkResponseDto */ 'key': string; + /** + * + * @type {string} + * @memberof SharedLinkResponseDto + */ + 'password': string | null; /** * * @type {boolean} * @memberof SharedLinkResponseDto */ 'showMetadata': boolean; + /** + * + * @type {string} + * @memberof SharedLinkResponseDto + */ + 'token'?: string | null; /** * * @type {SharedLinkType} @@ -13690,11 +13714,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur }, /** * + * @param {string} [password] + * @param {string} [token] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise => { + getMySharedLink: async (password?: string, token?: string, key?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/shared-link/me`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -13716,6 +13742,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (password !== undefined) { + localVarQueryParameter['password'] = password; + } + + if (token !== undefined) { + localVarQueryParameter['token'] = token; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -13959,12 +13993,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) { }, /** * + * @param {string} [password] + * @param {string} [token] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options); + async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -14053,7 +14089,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas * @throws {RequiredError} */ getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.getMySharedLink(requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -14142,6 +14178,20 @@ export interface SharedLinkApiCreateSharedLinkRequest { * @interface SharedLinkApiGetMySharedLinkRequest */ export interface SharedLinkApiGetMySharedLinkRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiGetMySharedLink + */ + readonly password?: string + + /** + * + * @type {string} + * @memberof SharedLinkApiGetMySharedLink + */ + readonly token?: string + /** * * @type {string} @@ -14274,7 +14324,7 @@ export class SharedLinkApi extends BaseAPI { * @memberof SharedLinkApi */ public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { - return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 66489c42ab..be576aa5c2 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -311,6 +311,8 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/lib/modules/shared_link/models/shared_link.dart b/mobile/lib/modules/shared_link/models/shared_link.dart index 5beabb566c..a107dd892a 100644 --- a/mobile/lib/modules/shared_link/models/shared_link.dart +++ b/mobile/lib/modules/shared_link/models/shared_link.dart @@ -9,6 +9,7 @@ class SharedLink { final bool allowUpload; final String? thumbAssetId; final String? description; + final String? password; final DateTime? expiresAt; final String key; final bool showMetadata; @@ -21,6 +22,7 @@ class SharedLink { required this.allowUpload, required this.thumbAssetId, required this.description, + required this.password, required this.expiresAt, required this.key, required this.showMetadata, @@ -34,6 +36,7 @@ class SharedLink { bool? allowDownload, bool? allowUpload, String? description, + String? password, DateTime? expiresAt, String? key, bool? showMetadata, @@ -46,6 +49,7 @@ class SharedLink { allowDownload: allowDownload ?? this.allowDownload, allowUpload: allowUpload ?? this.allowUpload, description: description ?? this.description, + password: password ?? this.password, expiresAt: expiresAt ?? this.expiresAt, key: key ?? this.key, showMetadata: showMetadata ?? this.showMetadata, @@ -58,6 +62,7 @@ class SharedLink { allowDownload = dto.allowDownload, allowUpload = dto.allowUpload, description = dto.description, + password = dto.password, expiresAt = dto.expiresAt, key = dto.key, showMetadata = dto.showMetadata, @@ -75,7 +80,7 @@ class SharedLink { @override String toString() => - 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)'; + 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)'; @override bool operator ==(Object other) => @@ -87,6 +92,7 @@ class SharedLink { other.allowDownload == allowDownload && other.allowUpload == allowUpload && other.description == description && + other.password == password && other.expiresAt == expiresAt && other.key == key && other.showMetadata == showMetadata && @@ -100,6 +106,7 @@ class SharedLink { allowDownload.hashCode ^ allowUpload.hashCode ^ description.hashCode ^ + password.hashCode ^ expiresAt.hashCode ^ key.hashCode ^ showMetadata.hashCode ^ diff --git a/mobile/lib/modules/shared_link/services/shared_link.service.dart b/mobile/lib/modules/shared_link/services/shared_link.service.dart index 2e28c20dac..3ea1d411b2 100644 --- a/mobile/lib/modules/shared_link/services/shared_link.service.dart +++ b/mobile/lib/modules/shared_link/services/shared_link.service.dart @@ -40,6 +40,7 @@ class SharedLinkService { required bool allowDownload, required bool allowUpload, String? description, + String? password, String? albumId, List? assetIds, DateTime? expiresAt, @@ -57,6 +58,7 @@ class SharedLinkService { allowUpload: allowUpload, expiresAt: expiresAt, description: description, + password: password, ); } else if (assetIds != null) { dto = SharedLinkCreateDto( @@ -66,6 +68,7 @@ class SharedLinkService { allowUpload: allowUpload, expiresAt: expiresAt, description: description, + password: password, assetIds: assetIds, ); } @@ -90,6 +93,7 @@ class SharedLinkService { required bool? allowUpload, bool? changeExpiry = false, String? description, + String? password, DateTime? expiresAt, }) async { try { @@ -101,6 +105,7 @@ class SharedLinkService { allowUpload: allowUpload, expiresAt: expiresAt, description: description, + password: password, changeExpiryTime: changeExpiry, ), ); diff --git a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart index d2a1aaeed4..499b2c29d3 100644 --- a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart +++ b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart @@ -30,6 +30,8 @@ class SharedLinkEditPage extends HookConsumerWidget { final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); + final passwordController = + useTextEditingController(text: existingLink?.password ?? ""); final showMetadata = useState(existingLink?.showMetadata ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true); final allowUpload = useState(existingLink?.allowUpload ?? false); @@ -113,6 +115,31 @@ class SharedLinkEditPage extends HookConsumerWidget { ); } + Widget buildPasswordField() { + return TextField( + controller: passwordController, + enabled: newShareLink.value.isEmpty, + autofocus: false, + decoration: InputDecoration( + labelText: 'shared_link_edit_password'.tr(), + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + color: themeData.primaryColor, + ), + floatingLabelBehavior: FloatingLabelBehavior.always, + border: const OutlineInputBorder(), + hintText: 'shared_link_edit_password_hint'.tr(), + hintStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)), + ), + ), + ); + } + Widget buildShowMetaButton() { return SwitchListTile.adaptive( value: showMetadata.value, @@ -229,7 +256,9 @@ class SharedLinkEditPage extends HookConsumerWidget { void copyLinkToClipboard() { Clipboard.setData( ClipboardData( - text: newShareLink.value, + text: passwordController.text.isEmpty + ? newShareLink.value + : "Link: ${newShareLink.value}\nPassword: ${passwordController.text}", ), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( @@ -302,6 +331,9 @@ class SharedLinkEditPage extends HookConsumerWidget { description: descriptionController.text.isEmpty ? null : descriptionController.text, + password: passwordController.text.isEmpty + ? null + : passwordController.text, expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), ); ref.invalidate(sharedLinksStateProvider); @@ -324,6 +356,7 @@ class SharedLinkEditPage extends HookConsumerWidget { bool? upload; bool? meta; String? desc; + String? password; DateTime? expiry; bool? changeExpiry; @@ -343,6 +376,10 @@ class SharedLinkEditPage extends HookConsumerWidget { desc = descriptionController.text; } + if (passwordController.text != existingLink!.password) { + password = passwordController.text; + } + if (editExpiry.value) { expiry = expiryAfter.value == 0 ? null : calculateExpiry(); changeExpiry = true; @@ -354,6 +391,7 @@ class SharedLinkEditPage extends HookConsumerWidget { allowDownload: download, allowUpload: upload, description: desc, + password: password, expiresAt: expiry, changeExpiry: changeExpiry, ); @@ -385,6 +423,10 @@ class SharedLinkEditPage extends HookConsumerWidget { padding: const EdgeInsets.all(padding), child: buildDescriptionField(), ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildPasswordField(), + ), Padding( padding: const EdgeInsets.only( left: padding, diff --git a/mobile/openapi/doc/SharedLinkApi.md b/mobile/openapi/doc/SharedLinkApi.md index 34b8e1e719..873ffc5825 100644 --- a/mobile/openapi/doc/SharedLinkApi.md +++ b/mobile/openapi/doc/SharedLinkApi.md @@ -185,7 +185,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMySharedLink** -> SharedLinkResponseDto getMySharedLink(key) +> SharedLinkResponseDto getMySharedLink(password, token, key) @@ -208,10 +208,12 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = SharedLinkApi(); +final password = password; // String | +final token = token_example; // String | final key = key_example; // String | try { - final result = api_instance.getMySharedLink(key); + final result = api_instance.getMySharedLink(password, token, key); print(result); } catch (e) { print('Exception when calling SharedLinkApi->getMySharedLink: $e\n'); @@ -222,6 +224,8 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- + **password** | **String**| | [optional] + **token** | **String**| | [optional] **key** | **String**| | [optional] ### Return type diff --git a/mobile/openapi/doc/SharedLinkCreateDto.md b/mobile/openapi/doc/SharedLinkCreateDto.md index 852610ae12..8f845dfa49 100644 --- a/mobile/openapi/doc/SharedLinkCreateDto.md +++ b/mobile/openapi/doc/SharedLinkCreateDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **assetIds** | **List** | | [optional] [default to const []] **description** | **String** | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional] +**password** | **String** | | [optional] **showMetadata** | **bool** | | [optional] [default to true] **type** | [**SharedLinkType**](SharedLinkType.md) | | diff --git a/mobile/openapi/doc/SharedLinkEditDto.md b/mobile/openapi/doc/SharedLinkEditDto.md index ccd0d3b543..36af31b475 100644 --- a/mobile/openapi/doc/SharedLinkEditDto.md +++ b/mobile/openapi/doc/SharedLinkEditDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **changeExpiryTime** | **bool** | Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. | [optional] **description** | **String** | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional] +**password** | **String** | | [optional] **showMetadata** | **bool** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index 24b76c86c8..89f7c7ac82 100644 --- a/mobile/openapi/doc/SharedLinkResponseDto.md +++ b/mobile/openapi/doc/SharedLinkResponseDto.md @@ -17,7 +17,9 @@ Name | Type | Description | Notes **expiresAt** | [**DateTime**](DateTime.md) | | **id** | **String** | | **key** | **String** | | +**password** | **String** | | **showMetadata** | **bool** | | +**token** | **String** | | [optional] **type** | [**SharedLinkType**](SharedLinkType.md) | | **userId** | **String** | | diff --git a/mobile/openapi/lib/api/shared_link_api.dart b/mobile/openapi/lib/api/shared_link_api.dart index 029f7bc8a7..661da45eb8 100644 --- a/mobile/openapi/lib/api/shared_link_api.dart +++ b/mobile/openapi/lib/api/shared_link_api.dart @@ -173,8 +173,12 @@ class SharedLinkApi { /// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response]. /// Parameters: /// + /// * [String] password: + /// + /// * [String] token: + /// /// * [String] key: - Future getMySharedLinkWithHttpInfo({ String? key, }) async { + Future getMySharedLinkWithHttpInfo({ String? password, String? token, String? key, }) async { // ignore: prefer_const_declarations final path = r'/shared-link/me'; @@ -185,6 +189,12 @@ class SharedLinkApi { final headerParams = {}; final formParams = {}; + if (password != null) { + queryParams.addAll(_queryParams('', 'password', password)); + } + if (token != null) { + queryParams.addAll(_queryParams('', 'token', token)); + } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } @@ -205,9 +215,13 @@ class SharedLinkApi { /// Parameters: /// + /// * [String] password: + /// + /// * [String] token: + /// /// * [String] key: - Future getMySharedLink({ String? key, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, ); + Future getMySharedLink({ String? password, String? token, String? key, }) async { + final response = await getMySharedLinkWithHttpInfo( password: password, token: token, key: key, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 8ce045ca1a..9f7b8edcd8 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -19,6 +19,7 @@ class SharedLinkCreateDto { this.assetIds = const [], this.description, this.expiresAt, + this.password, this.showMetadata = true, required this.type, }); @@ -47,6 +48,14 @@ class SharedLinkCreateDto { DateTime? expiresAt; + /// + /// 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; + bool showMetadata; SharedLinkType type; @@ -59,6 +68,7 @@ class SharedLinkCreateDto { other.assetIds == assetIds && other.description == description && other.expiresAt == expiresAt && + other.password == password && other.showMetadata == showMetadata && other.type == type; @@ -71,11 +81,12 @@ class SharedLinkCreateDto { (assetIds.hashCode) + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + + (password == null ? 0 : password!.hashCode) + (showMetadata.hashCode) + (type.hashCode); @override - String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata, type=$type]'; + String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, type=$type]'; Map toJson() { final json = {}; @@ -96,6 +107,11 @@ class SharedLinkCreateDto { json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; + } + if (this.password != null) { + json[r'password'] = this.password; + } else { + // json[r'password'] = null; } json[r'showMetadata'] = this.showMetadata; json[r'type'] = this.type; @@ -118,6 +134,7 @@ class SharedLinkCreateDto { : const [], description: mapValueOfType(json, r'description'), expiresAt: mapDateTime(json, r'expiresAt', ''), + password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata') ?? true, type: SharedLinkType.fromJson(json[r'type'])!, ); diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index 108734999d..4d7330f7d6 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -18,6 +18,7 @@ class SharedLinkEditDto { this.changeExpiryTime, this.description, this.expiresAt, + this.password, this.showMetadata, }); @@ -56,6 +57,14 @@ class SharedLinkEditDto { DateTime? expiresAt; + /// + /// 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 @@ -71,6 +80,7 @@ class SharedLinkEditDto { other.changeExpiryTime == changeExpiryTime && other.description == description && other.expiresAt == expiresAt && + other.password == password && other.showMetadata == showMetadata; @override @@ -81,10 +91,11 @@ class SharedLinkEditDto { (changeExpiryTime == null ? 0 : changeExpiryTime!.hashCode) + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + + (password == null ? 0 : password!.hashCode) + (showMetadata == null ? 0 : showMetadata!.hashCode); @override - String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata]'; + String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata]'; Map toJson() { final json = {}; @@ -113,6 +124,11 @@ class SharedLinkEditDto { } else { // json[r'expiresAt'] = null; } + if (this.password != null) { + json[r'password'] = this.password; + } else { + // json[r'password'] = null; + } if (this.showMetadata != null) { json[r'showMetadata'] = this.showMetadata; } else { @@ -134,6 +150,7 @@ class SharedLinkEditDto { changeExpiryTime: mapValueOfType(json, r'changeExpiryTime'), description: mapValueOfType(json, r'description'), expiresAt: mapDateTime(json, r'expiresAt', ''), + password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata'), ); } diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 33aa0577d7..fff364e547 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -22,7 +22,9 @@ class SharedLinkResponseDto { required this.expiresAt, required this.id, required this.key, + required this.password, required this.showMetadata, + this.token, required this.type, required this.userId, }); @@ -51,8 +53,12 @@ class SharedLinkResponseDto { String key; + String? password; + bool showMetadata; + String? token; + SharedLinkType type; String userId; @@ -68,7 +74,9 @@ class SharedLinkResponseDto { other.expiresAt == expiresAt && other.id == id && other.key == key && + other.password == password && other.showMetadata == showMetadata && + other.token == token && other.type == type && other.userId == userId; @@ -84,12 +92,14 @@ class SharedLinkResponseDto { (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (key.hashCode) + + (password == null ? 0 : password!.hashCode) + (showMetadata.hashCode) + + (token == null ? 0 : token!.hashCode) + (type.hashCode) + (userId.hashCode); @override - String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, showMetadata=$showMetadata, type=$type, userId=$userId]'; + String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId]'; Map toJson() { final json = {}; @@ -114,7 +124,17 @@ class SharedLinkResponseDto { } json[r'id'] = this.id; json[r'key'] = this.key; + if (this.password != null) { + json[r'password'] = this.password; + } else { + // json[r'password'] = null; + } json[r'showMetadata'] = this.showMetadata; + if (this.token != null) { + json[r'token'] = this.token; + } else { + // json[r'token'] = null; + } json[r'type'] = this.type; json[r'userId'] = this.userId; return json; @@ -137,7 +157,9 @@ class SharedLinkResponseDto { expiresAt: mapDateTime(json, r'expiresAt', ''), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, + password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata')!, + token: mapValueOfType(json, r'token'), type: SharedLinkType.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); @@ -195,6 +217,7 @@ class SharedLinkResponseDto { 'expiresAt', 'id', 'key', + 'password', 'showMetadata', 'type', 'userId', diff --git a/mobile/openapi/test/shared_link_api_test.dart b/mobile/openapi/test/shared_link_api_test.dart index 05843bad7a..edc2c55d0a 100644 --- a/mobile/openapi/test/shared_link_api_test.dart +++ b/mobile/openapi/test/shared_link_api_test.dart @@ -32,7 +32,7 @@ void main() { // TODO }); - //Future getMySharedLink({ String key }) async + //Future getMySharedLink({ String password, String token, String key }) async test('test getMySharedLink', () async { // TODO }); diff --git a/mobile/openapi/test/shared_link_create_dto_test.dart b/mobile/openapi/test/shared_link_create_dto_test.dart index e02cbe481e..df57e089f5 100644 --- a/mobile/openapi/test/shared_link_create_dto_test.dart +++ b/mobile/openapi/test/shared_link_create_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // String password + test('to test the property `password`', () async { + // TODO + }); + // bool showMetadata (default value: true) test('to test the property `showMetadata`', () async { // TODO diff --git a/mobile/openapi/test/shared_link_edit_dto_test.dart b/mobile/openapi/test/shared_link_edit_dto_test.dart index 893d12efe0..f5c45190c3 100644 --- a/mobile/openapi/test/shared_link_edit_dto_test.dart +++ b/mobile/openapi/test/shared_link_edit_dto_test.dart @@ -42,6 +42,11 @@ void main() { // TODO }); + // String password + test('to test the property `password`', () async { + // TODO + }); + // bool showMetadata test('to test the property `showMetadata`', () async { // TODO diff --git a/mobile/openapi/test/shared_link_response_dto_test.dart b/mobile/openapi/test/shared_link_response_dto_test.dart index fbe26b9ae3..0eb4ed50a7 100644 --- a/mobile/openapi/test/shared_link_response_dto_test.dart +++ b/mobile/openapi/test/shared_link_response_dto_test.dart @@ -61,11 +61,21 @@ void main() { // TODO }); + // String password + test('to test the property `password`', () async { + // TODO + }); + // bool showMetadata test('to test the property `showMetadata`', () async { // TODO }); + // String token + test('to test the property `token`', () async { + // TODO + }); + // SharedLinkType type test('to test the property `type`', () async { // TODO diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index b97bdc1cf0..ab9b16170b 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4263,6 +4263,23 @@ "get": { "operationId": "getMySharedLink", "parameters": [ + { + "name": "password", + "required": false, + "in": "query", + "example": "password", + "schema": { + "type": "string" + } + }, + { + "name": "token", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "key", "required": false, @@ -7910,6 +7927,9 @@ "nullable": true, "type": "string" }, + "password": { + "type": "string" + }, "showMetadata": { "default": true, "type": "boolean" @@ -7943,6 +7963,9 @@ "nullable": true, "type": "string" }, + "password": { + "type": "string" + }, "showMetadata": { "type": "boolean" } @@ -7985,9 +8008,17 @@ "key": { "type": "string" }, + "password": { + "nullable": true, + "type": "string" + }, "showMetadata": { "type": "boolean" }, + "token": { + "nullable": true, + "type": "string" + }, "type": { "$ref": "#/components/schemas/SharedLinkType" }, @@ -7999,6 +8030,7 @@ "type", "id", "description", + "password", "userId", "key", "createdAt", diff --git a/server/src/domain/auth/auth.constant.ts b/server/src/domain/auth/auth.constant.ts index 6f63cc1b3f..d237a19cd5 100644 --- a/server/src/domain/auth/auth.constant.ts +++ b/server/src/domain/auth/auth.constant.ts @@ -4,6 +4,7 @@ export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; export const IMMICH_API_KEY_NAME = 'api_key'; export const IMMICH_API_KEY_HEADER = 'x-api-key'; +export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; export enum AuthType { PASSWORD = 'password', OAUTH = 'oauth', diff --git a/server/src/domain/shared-link/shared-link-response.dto.ts b/server/src/domain/shared-link/shared-link-response.dto.ts index 4e35f65462..b16a578f41 100644 --- a/server/src/domain/shared-link/shared-link-response.dto.ts +++ b/server/src/domain/shared-link/shared-link-response.dto.ts @@ -7,6 +7,8 @@ import { AssetResponseDto, mapAsset } from '../asset'; export class SharedLinkResponseDto { id!: string; description!: string | null; + password!: string | null; + token?: string | null; userId!: string; key!: string; @@ -31,6 +33,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD return { id: sharedLink.id, description: sharedLink.description, + password: sharedLink.password, userId: sharedLink.userId, key: sharedLink.key.toString('base64url'), type: sharedLink.type, @@ -53,6 +56,7 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): Shar return { id: sharedLink.id, description: sharedLink.description, + password: sharedLink.password, userId: sharedLink.userId, key: sharedLink.key.toString('base64url'), type: sharedLink.type, diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts index ed38cf984d..bb5b61820a 100644 --- a/server/src/domain/shared-link/shared-link.dto.ts +++ b/server/src/domain/shared-link/shared-link.dto.ts @@ -19,6 +19,10 @@ export class SharedLinkCreateDto { @Optional() description?: string; + @IsString() + @Optional() + password?: string; + @IsDate() @Type(() => Date) @Optional({ nullable: true }) @@ -41,6 +45,9 @@ export class SharedLinkEditDto { @Optional() description?: string; + @Optional() + password?: string; + @Optional({ nullable: true }) expiresAt?: Date | null; @@ -62,3 +69,14 @@ export class SharedLinkEditDto { @IsBoolean() changeExpiryTime?: boolean; } + +export class SharedLinkPasswordDto { + @IsString() + @Optional() + @ApiProperty({ example: 'password' }) + password?: string; + + @IsString() + @Optional() + token?: string; +} diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index f902d7a68a..863e3a3534 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -1,5 +1,5 @@ import { SharedLinkType } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { IAccessRepositoryMock, albumStub, @@ -48,21 +48,28 @@ describe(SharedLinkService.name, () => { describe('getMine', () => { it('should only work for a public user', async () => { - await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException); + await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException); expect(shareMock.get).not.toHaveBeenCalled(); }); it('should return the shared link for the public user', async () => { const authDto = authStub.adminSharedLink; shareMock.get.mockResolvedValue(sharedLinkStub.valid); - await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid); + await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid); expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); }); it('should not return metadata', async () => { const authDto = authStub.adminSharedLinkNoExif; shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); - await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); + await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); + expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); + }); + + it('should throw an error for an password protected shared link', async () => { + const authDto = authStub.adminSharedLink; + shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); + await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); }); }); diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index 2cb87c8ebc..d3fd89661b 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -1,11 +1,11 @@ import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; import { AuthUserDto } from '../auth'; import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories'; import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto'; -import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto'; +import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto'; @Injectable() export class SharedLinkService { @@ -23,7 +23,7 @@ export class SharedLinkService { return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink)); } - async getMine(authUser: AuthUserDto): Promise { + async getMine(authUser: AuthUserDto, dto: SharedLinkPasswordDto): Promise { const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser; if (!isPublicUser || !id) { @@ -32,7 +32,15 @@ export class SharedLinkService { const sharedLink = await this.findOrFail(authUser, id); - return this.map(sharedLink, { withExif: isShowExif ?? true }); + let newToken; + if (sharedLink.password) { + newToken = this.validateAndRefreshToken(sharedLink, dto); + } + + return { + ...this.map(sharedLink, { withExif: isShowExif ?? true }), + token: newToken, + }; } async get(authUser: AuthUserDto, id: string): Promise { @@ -66,6 +74,7 @@ export class SharedLinkService { albumId: dto.albumId || null, assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity), description: dto.description || null, + password: dto.password, expiresAt: dto.expiresAt || null, allowUpload: dto.allowUpload ?? true, allowDownload: dto.allowDownload ?? true, @@ -81,6 +90,7 @@ export class SharedLinkService { id, userId: authUser.id, description: dto.description, + password: dto.password, expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt, allowUpload: dto.allowUpload, allowDownload: dto.allowDownload, @@ -159,4 +169,17 @@ export class SharedLinkService { private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); } + + private validateAndRefreshToken(sharedLink: SharedLinkEntity, dto: SharedLinkPasswordDto): string { + const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); + const sharedLinkTokens = dto.token?.split(',') || []; + if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) { + throw new UnauthorizedException('Invalid password'); + } + + if (!sharedLinkTokens.includes(token)) { + sharedLinkTokens.push(token); + } + return sharedLinkTokens.join(','); + } } diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index afd8c81ea3..15c0803dd4 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/immich/controllers/shared-link.controller.ts @@ -2,13 +2,16 @@ import { AssetIdsDto, AssetIdsResponseDto, AuthUserDto, + IMMICH_SHARED_LINK_ACCESS_COOKIE, SharedLinkCreateDto, SharedLinkEditDto, + SharedLinkPasswordDto, SharedLinkResponseDto, SharedLinkService, } from '@app/domain'; -import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Request, Response } from 'express'; import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -27,8 +30,25 @@ export class SharedLinkController { @SharedLinkRoute() @Get('me') - getMySharedLink(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getMine(authUser); + async getMySharedLink( + @AuthUser() authUser: AuthUserDto, + @Query() dto: SharedLinkPasswordDto, + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + ): Promise { + const sharedLinkToken = req.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; + if (sharedLinkToken) { + dto.token = sharedLinkToken; + } + const sharedLinkResponse = await this.service.getMine(authUser, dto); + if (sharedLinkResponse.token) { + res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, sharedLinkResponse.token, { + expires: new Date(Date.now() + 1000 * 60 * 60 * 24), + httpOnly: true, + sameSite: 'lax', + }); + } + return sharedLinkResponse; } @Get(':id') diff --git a/server/src/infra/entities/shared-link.entity.ts b/server/src/infra/entities/shared-link.entity.ts index e06635d6a6..1e42b8d2c2 100644 --- a/server/src/infra/entities/shared-link.entity.ts +++ b/server/src/infra/entities/shared-link.entity.ts @@ -21,6 +21,9 @@ export class SharedLinkEntity { @Column({ type: 'varchar', nullable: true }) description!: string | null; + @Column({ type: 'varchar', nullable: true }) + password!: string | null; + @Column() userId!: string; diff --git a/server/src/infra/migrations/1698290827089-AddPasswordToSharedLinks.ts b/server/src/infra/migrations/1698290827089-AddPasswordToSharedLinks.ts new file mode 100644 index 0000000000..b6906e3d05 --- /dev/null +++ b/server/src/infra/migrations/1698290827089-AddPasswordToSharedLinks.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddPasswordToSharedLinks1698290827089 implements MigrationInterface { + name = 'AddPasswordToSharedLinks1698290827089' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" ADD "password" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "password"`); + } + +} diff --git a/server/test/e2e/shared-link.e2e-spec.ts b/server/test/e2e/shared-link.e2e-spec.ts index 80d43c7c74..03eb9da7db 100644 --- a/server/test/e2e/shared-link.e2e-spec.ts +++ b/server/test/e2e/shared-link.e2e-spec.ts @@ -111,6 +111,34 @@ describe(`${PartnerController.name} (e2e)`, () => { expect(status).toBe(401); expect(body).toEqual(errorStub.invalidShareKey); }); + + it('should return unauthorized for password protected link', async () => { + const passwordProtectedLink = await api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: album.id, + password: 'foo', + }); + + const { status, body } = await request(server).get('/shared-link/me').query({ key: passwordProtectedLink.key }); + + expect(status).toBe(401); + expect(body).toEqual(errorStub.invalidSharePassword); + }); + + it('should get data for correct password protected link', async () => { + const passwordProtectedLink = await api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: album.id, + password: 'foo', + }); + + const { status, body } = await request(server) + .get('/shared-link/me') + .query({ key: passwordProtectedLink.key, password: 'foo' }); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + }); }); describe('GET /shared-link/:id', () => { diff --git a/server/test/fixtures/error.stub.ts b/server/test/fixtures/error.stub.ts index c37aad316c..cea514e26e 100644 --- a/server/test/fixtures/error.stub.ts +++ b/server/test/fixtures/error.stub.ts @@ -24,6 +24,11 @@ export const errorStub = { statusCode: 401, message: 'Invalid share key', }, + invalidSharePassword: { + error: 'Unauthorized', + statusCode: 401, + message: 'Invalid password', + }, badRequest: (message: any = null) => ({ error: 'Bad Request', statusCode: 400, diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index dd5771cf99..dd6eb52334 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -132,6 +132,7 @@ export const sharedLinkStub = { album: undefined, albumId: null, description: null, + password: null, assets: [], } as SharedLinkEntity), expired: Object.freeze({ @@ -146,6 +147,7 @@ export const sharedLinkStub = { allowDownload: true, showExif: true, description: null, + password: null, albumId: null, assets: [], } as SharedLinkEntity), @@ -161,6 +163,7 @@ export const sharedLinkStub = { allowDownload: false, showExif: false, description: null, + password: null, assets: [], albumId: 'album-123', album: { @@ -254,6 +257,22 @@ export const sharedLinkStub = { ], }, }), + passwordRequired: Object.freeze({ + id: '123', + userId: authStub.admin.id, + user: userStub.admin, + key: sharedLinkBytes, + type: SharedLinkType.ALBUM, + createdAt: today, + expiresAt: tomorrow, + allowUpload: true, + allowDownload: true, + showExif: true, + description: null, + password: 'password', + assets: [], + albumId: null, + }), }; export const sharedLinkResponseStub = { @@ -263,6 +282,7 @@ export const sharedLinkResponseStub = { assets: [], createdAt: today, description: null, + password: null, expiresAt: tomorrow, id: '123', key: sharedLinkBytes.toString('base64url'), @@ -277,6 +297,7 @@ export const sharedLinkResponseStub = { assets: [], createdAt: today, description: null, + password: null, expiresAt: yesterday, id: '123', key: sharedLinkBytes.toString('base64url'), @@ -292,6 +313,7 @@ export const sharedLinkResponseStub = { createdAt: today, expiresAt: tomorrow, description: null, + password: null, allowUpload: false, allowDownload: false, showMetadata: true, @@ -306,6 +328,7 @@ export const sharedLinkResponseStub = { createdAt: today, expiresAt: tomorrow, description: null, + password: null, allowUpload: false, allowDownload: false, showMetadata: false, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index e8cfda0b04..b1714e2764 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -3038,6 +3038,12 @@ export interface SharedLinkCreateDto { * @memberof SharedLinkCreateDto */ 'expiresAt'?: string | null; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'password'?: string; /** * * @type {boolean} @@ -3089,6 +3095,12 @@ export interface SharedLinkEditDto { * @memberof SharedLinkEditDto */ 'expiresAt'?: string | null; + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'password'?: string; /** * * @type {boolean} @@ -3156,12 +3168,24 @@ export interface SharedLinkResponseDto { * @memberof SharedLinkResponseDto */ 'key': string; + /** + * + * @type {string} + * @memberof SharedLinkResponseDto + */ + 'password': string | null; /** * * @type {boolean} * @memberof SharedLinkResponseDto */ 'showMetadata': boolean; + /** + * + * @type {string} + * @memberof SharedLinkResponseDto + */ + 'token'?: string | null; /** * * @type {SharedLinkType} @@ -13690,11 +13714,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur }, /** * + * @param {string} [password] + * @param {string} [token] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise => { + getMySharedLink: async (password?: string, token?: string, key?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/shared-link/me`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -13716,6 +13742,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (password !== undefined) { + localVarQueryParameter['password'] = password; + } + + if (token !== undefined) { + localVarQueryParameter['token'] = token; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -13959,12 +13993,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) { }, /** * + * @param {string} [password] + * @param {string} [token] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options); + async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -14053,7 +14089,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas * @throws {RequiredError} */ getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.getMySharedLink(requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -14142,6 +14178,20 @@ export interface SharedLinkApiCreateSharedLinkRequest { * @interface SharedLinkApiGetMySharedLinkRequest */ export interface SharedLinkApiGetMySharedLinkRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiGetMySharedLink + */ + readonly password?: string + + /** + * + * @type {string} + * @memberof SharedLinkApiGetMySharedLink + */ + readonly token?: string + /** * * @type {string} @@ -14274,7 +14324,7 @@ export class SharedLinkApi extends BaseAPI { * @memberof SharedLinkApi */ public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { - return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 774afc2dcd..5baefa1506 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -24,6 +24,7 @@ let allowUpload = false; let showMetadata = true; let expirationTime = ''; + let password = ''; let shouldChangeExpirationTime = false; let canCopyImagesToClipboard = true; const dispatch = createEventDispatcher(); @@ -40,6 +41,9 @@ if (editingLink.description) { description = editingLink.description; } + if (editingLink.password) { + password = editingLink.password; + } allowUpload = editingLink.allowUpload; allowDownload = editingLink.allowDownload; showMetadata = editingLink.showMetadata; @@ -66,6 +70,7 @@ expiresAt: expirationDate, allowUpload, description, + password, allowDownload, showMetadata, }, @@ -81,7 +86,7 @@ return; } - await copyToClipboard(sharedLink); + await copyToClipboard(password ? `Link: ${sharedLink}\nPassword: ${password}` : sharedLink); }; const getExpirationTimeInMillisecond = () => { @@ -119,6 +124,7 @@ id: editingLink.id, sharedLinkEditDto: { description, + password, expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, allowUpload, allowDownload, @@ -178,12 +184,16 @@

LINK OPTIONS

-
+
+
+ +
+
diff --git a/web/src/routes/(user)/share/[key]/+page.server.ts b/web/src/routes/(user)/share/[key]/+page.server.ts index d1d711fda2..5ba044df96 100644 --- a/web/src/routes/(user)/share/[key]/+page.server.ts +++ b/web/src/routes/(user)/share/[key]/+page.server.ts @@ -2,12 +2,14 @@ import featurePanelUrl from '$lib/assets/feature-panel.png'; import { api as clientApi, ThumbnailFormat } from '@api'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import type { AxiosError } from 'axios'; -export const load = (async ({ params, locals: { api } }) => { +export const load = (async ({ params, locals: { api }, cookies }) => { const { key } = params; + const token = cookies.get('immich_shared_link_token'); try { - const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key }); + const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key, token }); const assetCount = sharedLink.assets.length; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; @@ -23,6 +25,17 @@ export const load = (async ({ params, locals: { api } }) => { }, }; } catch (e) { + // handle unauthorized error + if ((e as AxiosError).response?.status === 401) { + return { + passwordRequired: true, + sharedLinkKey: key, + meta: { + title: 'Password Required', + }, + }; + } + throw error(404, { message: 'Invalid shared link', }); diff --git a/web/src/routes/(user)/share/[key]/+page.svelte b/web/src/routes/(user)/share/[key]/+page.svelte index 3982221697..c8ab740236 100644 --- a/web/src/routes/(user)/share/[key]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/+page.svelte @@ -1,20 +1,79 @@ -{#if sharedLink.type == SharedLinkType.Album} + + {title} + + +{#if passwordRequired} +
+ + + + +

IMMICH

+
+
+ + + + +
+
+
+
+
Password Required
+
+ Please enter the password to view this page. +
+
+ + +
+
+
+{/if} + +{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album} {/if} -{#if sharedLink.type == SharedLinkType.Individual} +{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}