diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e8cfda0b04735..b1714e2764327 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 66489c42ab7b7..be576aa5c2b79 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 5beabb566cb63..a107dd892a416 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 2e28c20dac752..3ea1d411b2006 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 d2a1aaeed4999..499b2c29d3f23 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 34b8e1e719911..873ffc5825e85 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 852610ae129ad..8f845dfa4985a 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 ccd0d3b543784..36af31b475483 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 24b76c86c8937..89f7c7ac825d3 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 029f7bc8a7346..661da45eb8d59 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 8ce045ca1a1a2..9f7b8edcd87b1 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 108734999d9e0..4d7330f7d6f40 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 33aa0577d74b5..fff364e547ffc 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 05843bad7abef..edc2c55d0a200 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 e02cbe481eb67..df57e089f54b5 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 893d12efe0882..f5c45190c3eab 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 fbe26b9ae3784..0eb4ed50a7092 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 b97bdc1cf0a0b..ab9b16170b8b4 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 6f63cc1b3fdea..d237a19cd53a9 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 4e35f65462e7d..b16a578f4152d 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 ed38cf984dc26..bb5b61820ac40 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 f902d7a68a89b..863e3a3534c79 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 2cb87c8ebcb8b..d3fd89661bf69 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 afd8c81ea30a9..15c0803dd44ae 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 e06635d6a62ac..1e42b8d2c29c9 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 0000000000000..b6906e3d054d2 --- /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 80d43c7c7481d..03eb9da7dbe5d 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 c37aad316c0cb..cea514e26ece6 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 dd5771cf99678..dd6eb523342a2 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 e8cfda0b04735..b1714e2764327 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 774afc2dcd692..5baefa1506349 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 d1d711fda24e2..5ba044df96c59 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 3982221697c68..c8ab740236478 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}