diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e8cfda0b0..b1714e276 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 66489c42a..be576aa5c 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 5beabb566..a107dd892 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 2e28c20da..3ea1d411b 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 d2a1aaeed..499b2c29d 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 34b8e1e71..873ffc582 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 852610ae1..8f845dfa4 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 ccd0d3b54..36af31b47 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 24b76c86c..89f7c7ac8 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 029f7bc8a..661da45eb 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 8ce045ca1..9f7b8edcd 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 108734999..4d7330f7d 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 33aa0577d..fff364e54 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 05843bad7..edc2c55d0 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 e02cbe481..df57e089f 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 893d12efe..f5c45190c 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 fbe26b9ae..0eb4ed50a 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 b97bdc1cf..ab9b16170 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 6f63cc1b3..d237a19cd 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 4e35f6546..b16a578f4 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 ed38cf984..bb5b61820 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 f902d7a68..863e3a353 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 2cb87c8eb..d3fd89661 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 afd8c81ea..15c0803dd 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 e06635d6a..1e42b8d2c 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 000000000..b6906e3d0 --- /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 80d43c7c7..03eb9da7d 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 c37aad316..cea514e26 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 dd5771cf9..dd6eb5233 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 e8cfda0b0..b1714e276 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 774afc2dc..5baefa150 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 d1d711fda..5ba044df9 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 398222169..c8ab74023 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}