mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(server,web,mobile): Add optional password option for share links. (#4655)
* feat(server,web,mobile): Add optional password option for share links. Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com> * feat(server,web): Update shared-link.controller and page.svelte for improved cookie handling and metadata updates. Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com> --------- Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com>
This commit is contained in:
parent
b34cbd881a
commit
8a6889529c
60
cli/src/api/open-api/api.ts
generated
60
cli/src/api/open-api/api.ts
generated
@ -3038,6 +3038,12 @@ export interface SharedLinkCreateDto {
|
|||||||
* @memberof SharedLinkCreateDto
|
* @memberof SharedLinkCreateDto
|
||||||
*/
|
*/
|
||||||
'expiresAt'?: string | null;
|
'expiresAt'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkCreateDto
|
||||||
|
*/
|
||||||
|
'password'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -3089,6 +3095,12 @@ export interface SharedLinkEditDto {
|
|||||||
* @memberof SharedLinkEditDto
|
* @memberof SharedLinkEditDto
|
||||||
*/
|
*/
|
||||||
'expiresAt'?: string | null;
|
'expiresAt'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkEditDto
|
||||||
|
*/
|
||||||
|
'password'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -3156,12 +3168,24 @@ export interface SharedLinkResponseDto {
|
|||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'key': string;
|
'key': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkResponseDto
|
||||||
|
*/
|
||||||
|
'password': string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'showMetadata': boolean;
|
'showMetadata': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkResponseDto
|
||||||
|
*/
|
||||||
|
'token'?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
@ -13690,11 +13714,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
* @param {string} [password]
|
||||||
|
* @param {string} [token]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getMySharedLink: async (password?: string, token?: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/shared-link/me`;
|
const localVarPath = `/shared-link/me`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -13716,6 +13742,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (password !== undefined) {
|
||||||
|
localVarQueryParameter['password'] = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token !== undefined) {
|
||||||
|
localVarQueryParameter['token'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
localVarQueryParameter['key'] = key;
|
localVarQueryParameter['key'] = key;
|
||||||
}
|
}
|
||||||
@ -13959,12 +13993,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
* @param {string} [password]
|
||||||
|
* @param {string} [token]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -14053,7 +14089,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
|
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
|
||||||
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
|
* @interface SharedLinkApiGetMySharedLinkRequest
|
||||||
*/
|
*/
|
||||||
export interface SharedLinkApiGetMySharedLinkRequest {
|
export interface SharedLinkApiGetMySharedLinkRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkApiGetMySharedLink
|
||||||
|
*/
|
||||||
|
readonly password?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkApiGetMySharedLink
|
||||||
|
*/
|
||||||
|
readonly token?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -14274,7 +14324,7 @@ export class SharedLinkApi extends BaseAPI {
|
|||||||
* @memberof SharedLinkApi
|
* @memberof SharedLinkApi
|
||||||
*/
|
*/
|
||||||
public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) {
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -311,6 +311,8 @@
|
|||||||
"shared_link_edit_change_expiry": "Change expiration time",
|
"shared_link_edit_change_expiry": "Change expiration time",
|
||||||
"shared_link_edit_description": "Description",
|
"shared_link_edit_description": "Description",
|
||||||
"shared_link_edit_description_hint": "Enter the share 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_show_meta": "Show metadata",
|
||||||
"shared_link_edit_submit_button": "Update link",
|
"shared_link_edit_submit_button": "Update link",
|
||||||
"shared_link_empty": "You don't have any shared links",
|
"shared_link_empty": "You don't have any shared links",
|
||||||
|
@ -9,6 +9,7 @@ class SharedLink {
|
|||||||
final bool allowUpload;
|
final bool allowUpload;
|
||||||
final String? thumbAssetId;
|
final String? thumbAssetId;
|
||||||
final String? description;
|
final String? description;
|
||||||
|
final String? password;
|
||||||
final DateTime? expiresAt;
|
final DateTime? expiresAt;
|
||||||
final String key;
|
final String key;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
@ -21,6 +22,7 @@ class SharedLink {
|
|||||||
required this.allowUpload,
|
required this.allowUpload,
|
||||||
required this.thumbAssetId,
|
required this.thumbAssetId,
|
||||||
required this.description,
|
required this.description,
|
||||||
|
required this.password,
|
||||||
required this.expiresAt,
|
required this.expiresAt,
|
||||||
required this.key,
|
required this.key,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
@ -34,6 +36,7 @@ class SharedLink {
|
|||||||
bool? allowDownload,
|
bool? allowDownload,
|
||||||
bool? allowUpload,
|
bool? allowUpload,
|
||||||
String? description,
|
String? description,
|
||||||
|
String? password,
|
||||||
DateTime? expiresAt,
|
DateTime? expiresAt,
|
||||||
String? key,
|
String? key,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
@ -46,6 +49,7 @@ class SharedLink {
|
|||||||
allowDownload: allowDownload ?? this.allowDownload,
|
allowDownload: allowDownload ?? this.allowDownload,
|
||||||
allowUpload: allowUpload ?? this.allowUpload,
|
allowUpload: allowUpload ?? this.allowUpload,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
|
password: password ?? this.password,
|
||||||
expiresAt: expiresAt ?? this.expiresAt,
|
expiresAt: expiresAt ?? this.expiresAt,
|
||||||
key: key ?? this.key,
|
key: key ?? this.key,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
@ -58,6 +62,7 @@ class SharedLink {
|
|||||||
allowDownload = dto.allowDownload,
|
allowDownload = dto.allowDownload,
|
||||||
allowUpload = dto.allowUpload,
|
allowUpload = dto.allowUpload,
|
||||||
description = dto.description,
|
description = dto.description,
|
||||||
|
password = dto.password,
|
||||||
expiresAt = dto.expiresAt,
|
expiresAt = dto.expiresAt,
|
||||||
key = dto.key,
|
key = dto.key,
|
||||||
showMetadata = dto.showMetadata,
|
showMetadata = dto.showMetadata,
|
||||||
@ -75,7 +80,7 @@ class SharedLink {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
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
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@ -87,6 +92,7 @@ class SharedLink {
|
|||||||
other.allowDownload == allowDownload &&
|
other.allowDownload == allowDownload &&
|
||||||
other.allowUpload == allowUpload &&
|
other.allowUpload == allowUpload &&
|
||||||
other.description == description &&
|
other.description == description &&
|
||||||
|
other.password == password &&
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.key == key &&
|
other.key == key &&
|
||||||
other.showMetadata == showMetadata &&
|
other.showMetadata == showMetadata &&
|
||||||
@ -100,6 +106,7 @@ class SharedLink {
|
|||||||
allowDownload.hashCode ^
|
allowDownload.hashCode ^
|
||||||
allowUpload.hashCode ^
|
allowUpload.hashCode ^
|
||||||
description.hashCode ^
|
description.hashCode ^
|
||||||
|
password.hashCode ^
|
||||||
expiresAt.hashCode ^
|
expiresAt.hashCode ^
|
||||||
key.hashCode ^
|
key.hashCode ^
|
||||||
showMetadata.hashCode ^
|
showMetadata.hashCode ^
|
||||||
|
@ -40,6 +40,7 @@ class SharedLinkService {
|
|||||||
required bool allowDownload,
|
required bool allowDownload,
|
||||||
required bool allowUpload,
|
required bool allowUpload,
|
||||||
String? description,
|
String? description,
|
||||||
|
String? password,
|
||||||
String? albumId,
|
String? albumId,
|
||||||
List<String>? assetIds,
|
List<String>? assetIds,
|
||||||
DateTime? expiresAt,
|
DateTime? expiresAt,
|
||||||
@ -57,6 +58,7 @@ class SharedLinkService {
|
|||||||
allowUpload: allowUpload,
|
allowUpload: allowUpload,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
|
password: password,
|
||||||
);
|
);
|
||||||
} else if (assetIds != null) {
|
} else if (assetIds != null) {
|
||||||
dto = SharedLinkCreateDto(
|
dto = SharedLinkCreateDto(
|
||||||
@ -66,6 +68,7 @@ class SharedLinkService {
|
|||||||
allowUpload: allowUpload,
|
allowUpload: allowUpload,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
|
password: password,
|
||||||
assetIds: assetIds,
|
assetIds: assetIds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -90,6 +93,7 @@ class SharedLinkService {
|
|||||||
required bool? allowUpload,
|
required bool? allowUpload,
|
||||||
bool? changeExpiry = false,
|
bool? changeExpiry = false,
|
||||||
String? description,
|
String? description,
|
||||||
|
String? password,
|
||||||
DateTime? expiresAt,
|
DateTime? expiresAt,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
@ -101,6 +105,7 @@ class SharedLinkService {
|
|||||||
allowUpload: allowUpload,
|
allowUpload: allowUpload,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
|
password: password,
|
||||||
changeExpiryTime: changeExpiry,
|
changeExpiryTime: changeExpiry,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -30,6 +30,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
final descriptionController =
|
final descriptionController =
|
||||||
useTextEditingController(text: existingLink?.description ?? "");
|
useTextEditingController(text: existingLink?.description ?? "");
|
||||||
final descriptionFocusNode = useFocusNode();
|
final descriptionFocusNode = useFocusNode();
|
||||||
|
final passwordController =
|
||||||
|
useTextEditingController(text: existingLink?.password ?? "");
|
||||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
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() {
|
Widget buildShowMetaButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: showMetadata.value,
|
value: showMetadata.value,
|
||||||
@ -229,7 +256,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
void copyLinkToClipboard() {
|
void copyLinkToClipboard() {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: newShareLink.value,
|
text: passwordController.text.isEmpty
|
||||||
|
? newShareLink.value
|
||||||
|
: "Link: ${newShareLink.value}\nPassword: ${passwordController.text}",
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -302,6 +331,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
description: descriptionController.text.isEmpty
|
description: descriptionController.text.isEmpty
|
||||||
? null
|
? null
|
||||||
: descriptionController.text,
|
: descriptionController.text,
|
||||||
|
password: passwordController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: passwordController.text,
|
||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
);
|
);
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
@ -324,6 +356,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
bool? upload;
|
bool? upload;
|
||||||
bool? meta;
|
bool? meta;
|
||||||
String? desc;
|
String? desc;
|
||||||
|
String? password;
|
||||||
DateTime? expiry;
|
DateTime? expiry;
|
||||||
bool? changeExpiry;
|
bool? changeExpiry;
|
||||||
|
|
||||||
@ -343,6 +376,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
desc = descriptionController.text;
|
desc = descriptionController.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (passwordController.text != existingLink!.password) {
|
||||||
|
password = passwordController.text;
|
||||||
|
}
|
||||||
|
|
||||||
if (editExpiry.value) {
|
if (editExpiry.value) {
|
||||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||||
changeExpiry = true;
|
changeExpiry = true;
|
||||||
@ -354,6 +391,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
allowDownload: download,
|
allowDownload: download,
|
||||||
allowUpload: upload,
|
allowUpload: upload,
|
||||||
description: desc,
|
description: desc,
|
||||||
|
password: password,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry,
|
||||||
changeExpiry: changeExpiry,
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
@ -385,6 +423,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(padding),
|
padding: const EdgeInsets.all(padding),
|
||||||
child: buildDescriptionField(),
|
child: buildDescriptionField(),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(padding),
|
||||||
|
child: buildPasswordField(),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: padding,
|
left: padding,
|
||||||
|
BIN
mobile/openapi/doc/SharedLinkApi.md
generated
BIN
mobile/openapi/doc/SharedLinkApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkCreateDto.md
generated
BIN
mobile/openapi/doc/SharedLinkCreateDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkEditDto.md
generated
BIN
mobile/openapi/doc/SharedLinkEditDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkResponseDto.md
generated
BIN
mobile/openapi/doc/SharedLinkResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/shared_link_api.dart
generated
BIN
mobile/openapi/lib/api/shared_link_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_api_test.dart
generated
BIN
mobile/openapi/test/shared_link_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_create_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_create_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_edit_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_edit_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_response_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_response_dto_test.dart
generated
Binary file not shown.
@ -4263,6 +4263,23 @@
|
|||||||
"get": {
|
"get": {
|
||||||
"operationId": "getMySharedLink",
|
"operationId": "getMySharedLink",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"example": "password",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "key",
|
"name": "key",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -7910,6 +7927,9 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@ -7943,6 +7963,9 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
@ -7985,9 +8008,17 @@
|
|||||||
"key": {
|
"key": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"password": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"showMetadata": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"token": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"$ref": "#/components/schemas/SharedLinkType"
|
"$ref": "#/components/schemas/SharedLinkType"
|
||||||
},
|
},
|
||||||
@ -7999,6 +8030,7 @@
|
|||||||
"type",
|
"type",
|
||||||
"id",
|
"id",
|
||||||
"description",
|
"description",
|
||||||
|
"password",
|
||||||
"userId",
|
"userId",
|
||||||
"key",
|
"key",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -4,6 +4,7 @@ export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
|
|||||||
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
|
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
|
||||||
export const IMMICH_API_KEY_NAME = 'api_key';
|
export const IMMICH_API_KEY_NAME = 'api_key';
|
||||||
export const IMMICH_API_KEY_HEADER = 'x-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 {
|
export enum AuthType {
|
||||||
PASSWORD = 'password',
|
PASSWORD = 'password',
|
||||||
OAUTH = 'oauth',
|
OAUTH = 'oauth',
|
||||||
|
@ -7,6 +7,8 @@ import { AssetResponseDto, mapAsset } from '../asset';
|
|||||||
export class SharedLinkResponseDto {
|
export class SharedLinkResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
description!: string | null;
|
description!: string | null;
|
||||||
|
password!: string | null;
|
||||||
|
token?: string | null;
|
||||||
userId!: string;
|
userId!: string;
|
||||||
key!: string;
|
key!: string;
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
|||||||
return {
|
return {
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
description: sharedLink.description,
|
description: sharedLink.description,
|
||||||
|
password: sharedLink.password,
|
||||||
userId: sharedLink.userId,
|
userId: sharedLink.userId,
|
||||||
key: sharedLink.key.toString('base64url'),
|
key: sharedLink.key.toString('base64url'),
|
||||||
type: sharedLink.type,
|
type: sharedLink.type,
|
||||||
@ -53,6 +56,7 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): Shar
|
|||||||
return {
|
return {
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
description: sharedLink.description,
|
description: sharedLink.description,
|
||||||
|
password: sharedLink.password,
|
||||||
userId: sharedLink.userId,
|
userId: sharedLink.userId,
|
||||||
key: sharedLink.key.toString('base64url'),
|
key: sharedLink.key.toString('base64url'),
|
||||||
type: sharedLink.type,
|
type: sharedLink.type,
|
||||||
|
@ -19,6 +19,10 @@ export class SharedLinkCreateDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
@IsDate()
|
@IsDate()
|
||||||
@Type(() => Date)
|
@Type(() => Date)
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@ -41,6 +45,9 @@ export class SharedLinkEditDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
expiresAt?: Date | null;
|
expiresAt?: Date | null;
|
||||||
|
|
||||||
@ -62,3 +69,14 @@ export class SharedLinkEditDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
changeExpiryTime?: boolean;
|
changeExpiryTime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SharedLinkPasswordDto {
|
||||||
|
@IsString()
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ example: 'password' })
|
||||||
|
password?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional()
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SharedLinkType } from '@app/infra/entities';
|
import { SharedLinkType } from '@app/infra/entities';
|
||||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
IAccessRepositoryMock,
|
IAccessRepositoryMock,
|
||||||
albumStub,
|
albumStub,
|
||||||
@ -48,21 +48,28 @@ describe(SharedLinkService.name, () => {
|
|||||||
|
|
||||||
describe('getMine', () => {
|
describe('getMine', () => {
|
||||||
it('should only work for a public user', async () => {
|
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();
|
expect(shareMock.get).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the shared link for the public user', async () => {
|
it('should return the shared link for the public user', async () => {
|
||||||
const authDto = authStub.adminSharedLink;
|
const authDto = authStub.adminSharedLink;
|
||||||
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
|
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);
|
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return metadata', async () => {
|
it('should not return metadata', async () => {
|
||||||
const authDto = authStub.adminSharedLinkNoExif;
|
const authDto = authStub.adminSharedLinkNoExif;
|
||||||
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
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);
|
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entities';
|
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 { AccessCore, Permission } from '../access';
|
||||||
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
|
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
|
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
|
||||||
import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto';
|
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()
|
@Injectable()
|
||||||
export class SharedLinkService {
|
export class SharedLinkService {
|
||||||
@ -23,7 +23,7 @@ export class SharedLinkService {
|
|||||||
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
|
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
async getMine(authUser: AuthUserDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
|
||||||
const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser;
|
const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser;
|
||||||
|
|
||||||
if (!isPublicUser || !id) {
|
if (!isPublicUser || !id) {
|
||||||
@ -32,7 +32,15 @@ export class SharedLinkService {
|
|||||||
|
|
||||||
const sharedLink = await this.findOrFail(authUser, id);
|
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<SharedLinkResponseDto> {
|
async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> {
|
||||||
@ -66,6 +74,7 @@ export class SharedLinkService {
|
|||||||
albumId: dto.albumId || null,
|
albumId: dto.albumId || null,
|
||||||
assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity),
|
assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity),
|
||||||
description: dto.description || null,
|
description: dto.description || null,
|
||||||
|
password: dto.password,
|
||||||
expiresAt: dto.expiresAt || null,
|
expiresAt: dto.expiresAt || null,
|
||||||
allowUpload: dto.allowUpload ?? true,
|
allowUpload: dto.allowUpload ?? true,
|
||||||
allowDownload: dto.allowDownload ?? true,
|
allowDownload: dto.allowDownload ?? true,
|
||||||
@ -81,6 +90,7 @@ export class SharedLinkService {
|
|||||||
id,
|
id,
|
||||||
userId: authUser.id,
|
userId: authUser.id,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
password: dto.password,
|
||||||
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
|
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
|
||||||
allowUpload: dto.allowUpload,
|
allowUpload: dto.allowUpload,
|
||||||
allowDownload: dto.allowDownload,
|
allowDownload: dto.allowDownload,
|
||||||
@ -159,4 +169,17 @@ export class SharedLinkService {
|
|||||||
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
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(',');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,16 @@ import {
|
|||||||
AssetIdsDto,
|
AssetIdsDto,
|
||||||
AssetIdsResponseDto,
|
AssetIdsResponseDto,
|
||||||
AuthUserDto,
|
AuthUserDto,
|
||||||
|
IMMICH_SHARED_LINK_ACCESS_COOKIE,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
SharedLinkEditDto,
|
SharedLinkEditDto,
|
||||||
|
SharedLinkPasswordDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkService,
|
SharedLinkService,
|
||||||
} from '@app/domain';
|
} 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 { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
|
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
@ -27,8 +30,25 @@ export class SharedLinkController {
|
|||||||
|
|
||||||
@SharedLinkRoute()
|
@SharedLinkRoute()
|
||||||
@Get('me')
|
@Get('me')
|
||||||
getMySharedLink(@AuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
async getMySharedLink(
|
||||||
return this.service.getMine(authUser);
|
@AuthUser() authUser: AuthUserDto,
|
||||||
|
@Query() dto: SharedLinkPasswordDto,
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<SharedLinkResponseDto> {
|
||||||
|
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')
|
@Get(':id')
|
||||||
|
@ -21,6 +21,9 @@ export class SharedLinkEntity {
|
|||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
description!: string | null;
|
description!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
password!: string | null;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddPasswordToSharedLinks1698290827089 implements MigrationInterface {
|
||||||
|
name = 'AddPasswordToSharedLinks1698290827089'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_links" ADD "password" character varying`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "password"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -111,6 +111,34 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
|||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorStub.invalidShareKey);
|
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', () => {
|
describe('GET /shared-link/:id', () => {
|
||||||
|
5
server/test/fixtures/error.stub.ts
vendored
5
server/test/fixtures/error.stub.ts
vendored
@ -24,6 +24,11 @@ export const errorStub = {
|
|||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Invalid share key',
|
message: 'Invalid share key',
|
||||||
},
|
},
|
||||||
|
invalidSharePassword: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid password',
|
||||||
|
},
|
||||||
badRequest: (message: any = null) => ({
|
badRequest: (message: any = null) => ({
|
||||||
error: 'Bad Request',
|
error: 'Bad Request',
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
|
23
server/test/fixtures/shared-link.stub.ts
vendored
23
server/test/fixtures/shared-link.stub.ts
vendored
@ -132,6 +132,7 @@ export const sharedLinkStub = {
|
|||||||
album: undefined,
|
album: undefined,
|
||||||
albumId: null,
|
albumId: null,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
assets: [],
|
assets: [],
|
||||||
} as SharedLinkEntity),
|
} as SharedLinkEntity),
|
||||||
expired: Object.freeze({
|
expired: Object.freeze({
|
||||||
@ -146,6 +147,7 @@ export const sharedLinkStub = {
|
|||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
showExif: true,
|
showExif: true,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
albumId: null,
|
albumId: null,
|
||||||
assets: [],
|
assets: [],
|
||||||
} as SharedLinkEntity),
|
} as SharedLinkEntity),
|
||||||
@ -161,6 +163,7 @@ export const sharedLinkStub = {
|
|||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showExif: false,
|
showExif: false,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
assets: [],
|
assets: [],
|
||||||
albumId: 'album-123',
|
albumId: 'album-123',
|
||||||
album: {
|
album: {
|
||||||
@ -254,6 +257,22 @@ export const sharedLinkStub = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
passwordRequired: Object.freeze<SharedLinkEntity>({
|
||||||
|
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 = {
|
export const sharedLinkResponseStub = {
|
||||||
@ -263,6 +282,7 @@ export const sharedLinkResponseStub = {
|
|||||||
assets: [],
|
assets: [],
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
expiresAt: tomorrow,
|
expiresAt: tomorrow,
|
||||||
id: '123',
|
id: '123',
|
||||||
key: sharedLinkBytes.toString('base64url'),
|
key: sharedLinkBytes.toString('base64url'),
|
||||||
@ -277,6 +297,7 @@ export const sharedLinkResponseStub = {
|
|||||||
assets: [],
|
assets: [],
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
expiresAt: yesterday,
|
expiresAt: yesterday,
|
||||||
id: '123',
|
id: '123',
|
||||||
key: sharedLinkBytes.toString('base64url'),
|
key: sharedLinkBytes.toString('base64url'),
|
||||||
@ -292,6 +313,7 @@ export const sharedLinkResponseStub = {
|
|||||||
createdAt: today,
|
createdAt: today,
|
||||||
expiresAt: tomorrow,
|
expiresAt: tomorrow,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
allowUpload: false,
|
allowUpload: false,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showMetadata: true,
|
showMetadata: true,
|
||||||
@ -306,6 +328,7 @@ export const sharedLinkResponseStub = {
|
|||||||
createdAt: today,
|
createdAt: today,
|
||||||
expiresAt: tomorrow,
|
expiresAt: tomorrow,
|
||||||
description: null,
|
description: null,
|
||||||
|
password: null,
|
||||||
allowUpload: false,
|
allowUpload: false,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showMetadata: false,
|
showMetadata: false,
|
||||||
|
60
web/src/api/open-api/api.ts
generated
60
web/src/api/open-api/api.ts
generated
@ -3038,6 +3038,12 @@ export interface SharedLinkCreateDto {
|
|||||||
* @memberof SharedLinkCreateDto
|
* @memberof SharedLinkCreateDto
|
||||||
*/
|
*/
|
||||||
'expiresAt'?: string | null;
|
'expiresAt'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkCreateDto
|
||||||
|
*/
|
||||||
|
'password'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -3089,6 +3095,12 @@ export interface SharedLinkEditDto {
|
|||||||
* @memberof SharedLinkEditDto
|
* @memberof SharedLinkEditDto
|
||||||
*/
|
*/
|
||||||
'expiresAt'?: string | null;
|
'expiresAt'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkEditDto
|
||||||
|
*/
|
||||||
|
'password'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -3156,12 +3168,24 @@ export interface SharedLinkResponseDto {
|
|||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'key': string;
|
'key': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkResponseDto
|
||||||
|
*/
|
||||||
|
'password': string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'showMetadata': boolean;
|
'showMetadata': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkResponseDto
|
||||||
|
*/
|
||||||
|
'token'?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
@ -13690,11 +13714,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
* @param {string} [password]
|
||||||
|
* @param {string} [token]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getMySharedLink: async (password?: string, token?: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/shared-link/me`;
|
const localVarPath = `/shared-link/me`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -13716,6 +13742,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (password !== undefined) {
|
||||||
|
localVarQueryParameter['password'] = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token !== undefined) {
|
||||||
|
localVarQueryParameter['token'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
localVarQueryParameter['key'] = key;
|
localVarQueryParameter['key'] = key;
|
||||||
}
|
}
|
||||||
@ -13959,12 +13993,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
* @param {string} [password]
|
||||||
|
* @param {string} [token]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -14053,7 +14089,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
|
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
|
||||||
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
|
* @interface SharedLinkApiGetMySharedLinkRequest
|
||||||
*/
|
*/
|
||||||
export interface SharedLinkApiGetMySharedLinkRequest {
|
export interface SharedLinkApiGetMySharedLinkRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkApiGetMySharedLink
|
||||||
|
*/
|
||||||
|
readonly password?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SharedLinkApiGetMySharedLink
|
||||||
|
*/
|
||||||
|
readonly token?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -14274,7 +14324,7 @@ export class SharedLinkApi extends BaseAPI {
|
|||||||
* @memberof SharedLinkApi
|
* @memberof SharedLinkApi
|
||||||
*/
|
*/
|
||||||
public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) {
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
let allowUpload = false;
|
let allowUpload = false;
|
||||||
let showMetadata = true;
|
let showMetadata = true;
|
||||||
let expirationTime = '';
|
let expirationTime = '';
|
||||||
|
let password = '';
|
||||||
let shouldChangeExpirationTime = false;
|
let shouldChangeExpirationTime = false;
|
||||||
let canCopyImagesToClipboard = true;
|
let canCopyImagesToClipboard = true;
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -40,6 +41,9 @@
|
|||||||
if (editingLink.description) {
|
if (editingLink.description) {
|
||||||
description = editingLink.description;
|
description = editingLink.description;
|
||||||
}
|
}
|
||||||
|
if (editingLink.password) {
|
||||||
|
password = editingLink.password;
|
||||||
|
}
|
||||||
allowUpload = editingLink.allowUpload;
|
allowUpload = editingLink.allowUpload;
|
||||||
allowDownload = editingLink.allowDownload;
|
allowDownload = editingLink.allowDownload;
|
||||||
showMetadata = editingLink.showMetadata;
|
showMetadata = editingLink.showMetadata;
|
||||||
@ -66,6 +70,7 @@
|
|||||||
expiresAt: expirationDate,
|
expiresAt: expirationDate,
|
||||||
allowUpload,
|
allowUpload,
|
||||||
description,
|
description,
|
||||||
|
password,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
},
|
},
|
||||||
@ -81,7 +86,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await copyToClipboard(sharedLink);
|
await copyToClipboard(password ? `Link: ${sharedLink}\nPassword: ${password}` : sharedLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExpirationTimeInMillisecond = () => {
|
const getExpirationTimeInMillisecond = () => {
|
||||||
@ -119,6 +124,7 @@
|
|||||||
id: editingLink.id,
|
id: editingLink.id,
|
||||||
sharedLinkEditDto: {
|
sharedLinkEditDto: {
|
||||||
description,
|
description,
|
||||||
|
password,
|
||||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||||
allowUpload,
|
allowUpload,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
@ -178,12 +184,16 @@
|
|||||||
<div class="mb-2 mt-4">
|
<div class="mb-2 mt-4">
|
||||||
<p class="text-xs">LINK OPTIONS</p>
|
<p class="text-xs">LINK OPTIONS</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40">
|
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 max-h-[330px] overflow-y-scroll">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<SettingInputField inputType={SettingInputFieldType.TEXT} label="Description" bind:value={description} />
|
<SettingInputField inputType={SettingInputFieldType.TEXT} label="Description" bind:value={description} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<SettingInputField inputType={SettingInputFieldType.TEXT} label="Password" bind:value={password} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<SettingSwitch bind:checked={showMetadata} title={'Show metadata'} />
|
<SettingSwitch bind:checked={showMetadata} title={'Show metadata'} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,12 +2,14 @@ import featurePanelUrl from '$lib/assets/feature-panel.png';
|
|||||||
import { api as clientApi, ThumbnailFormat } from '@api';
|
import { api as clientApi, ThumbnailFormat } from '@api';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
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 { key } = params;
|
||||||
|
const token = cookies.get('immich_shared_link_token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
|
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key, token });
|
||||||
|
|
||||||
const assetCount = sharedLink.assets.length;
|
const assetCount = sharedLink.assets.length;
|
||||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||||
@ -23,6 +25,17 @@ export const load = (async ({ params, locals: { api } }) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// handle unauthorized error
|
||||||
|
if ((e as AxiosError).response?.status === 401) {
|
||||||
|
return {
|
||||||
|
passwordRequired: true,
|
||||||
|
sharedLinkKey: key,
|
||||||
|
meta: {
|
||||||
|
title: 'Password Required',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw error(404, {
|
throw error(404, {
|
||||||
message: 'Invalid shared link',
|
message: 'Invalid shared link',
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,79 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||||
import { SharedLinkType } from '@api';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
|
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||||
|
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||||
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import { api, SharedLinkType } from '@api';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
const { sharedLink } = data;
|
let { sharedLink, passwordRequired, sharedLinkKey: key } = data;
|
||||||
|
let { title, description } = data.meta;
|
||||||
|
|
||||||
let isOwned = data.user ? data.user.id === sharedLink.userId : false;
|
let isOwned = data.user ? data.user.id === sharedLink?.userId : false;
|
||||||
|
let password = '';
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const result = await api.sharedLinkApi.getMySharedLink({ password, key });
|
||||||
|
passwordRequired = false;
|
||||||
|
sharedLink = result.data;
|
||||||
|
isOwned = data.user ? data.user.id === sharedLink.userId : false;
|
||||||
|
title = (sharedLink.album ? sharedLink.album.albumName : 'Public Share') + ' - Immich';
|
||||||
|
description = sharedLink.description || `${sharedLink.assets.length} shared photos & videos.`;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Failed to get shared link');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if sharedLink.type == SharedLinkType.Album}
|
<svelte:head>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
</svelte:head>
|
||||||
|
{#if passwordRequired}
|
||||||
|
<header>
|
||||||
|
<ControlAppBar showBackButton={false}>
|
||||||
|
<svelte:fragment slot="leading">
|
||||||
|
<a
|
||||||
|
data-sveltekit-preload-data="hover"
|
||||||
|
class="ml-6 flex place-items-center gap-2 hover:cursor-pointer"
|
||||||
|
href="https://immich.app"
|
||||||
|
>
|
||||||
|
<ImmichLogo height={30} width={30} />
|
||||||
|
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
|
||||||
|
</a>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="trailing">
|
||||||
|
<ThemeButton />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ControlAppBar>
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center mt-20">
|
||||||
|
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">Password Required</div>
|
||||||
|
<div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
|
||||||
|
Please enter the password to view this page.
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<input type="password" class="immich-form-input mr-2" placeholder="Password" bind:value={password} />
|
||||||
|
<Button on:click={handlePasswordSubmit}>Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
||||||
<AlbumViewer {sharedLink} />
|
<AlbumViewer {sharedLink} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sharedLink.type == SharedLinkType.Individual}
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
||||||
<div class="immich-scrollbar">
|
<div class="immich-scrollbar">
|
||||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user