From 35767591d282a746c7f619967b6aee2909632162 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 11 Nov 2023 15:06:19 -0600 Subject: [PATCH] feat(web): show partners assets on the main timeline (#4933) --- cli/src/api/open-api/api.ts | 264 +++++++++++++++++- .../providers/authentication.provider.dart | 7 +- .../partner/services/partner.service.dart | 2 +- .../lib/routing/tab_navigation_observer.dart | 5 +- mobile/lib/shared/models/user.dart | 24 +- mobile/lib/shared/models/user.g.dart | Bin 48644 -> 50751 bytes mobile/lib/shared/services/user.service.dart | 2 +- mobile/openapi/.openapi-generator/FILES | 6 + mobile/openapi/README.md | Bin 23009 -> 23205 bytes mobile/openapi/doc/AssetApi.md | Bin 66863 -> 67085 bytes mobile/openapi/doc/PartnerApi.md | Bin 6159 -> 8370 bytes mobile/openapi/doc/PartnerResponseDto.md | Bin 0 -> 959 bytes mobile/openapi/doc/UpdatePartnerDto.md | Bin 0 -> 414 bytes mobile/openapi/lib/api.dart | Bin 7494 -> 7572 bytes mobile/openapi/lib/api/asset_api.dart | Bin 59215 -> 59715 bytes mobile/openapi/lib/api/partner_api.dart | Bin 4686 -> 6493 bytes mobile/openapi/lib/api_client.dart | Bin 22091 -> 22263 bytes .../lib/model/partner_response_dto.dart | Bin 0 -> 7973 bytes .../openapi/lib/model/update_partner_dto.dart | Bin 0 -> 2834 bytes mobile/openapi/test/asset_api_test.dart | Bin 5930 -> 5968 bytes mobile/openapi/test/partner_api_test.dart | Bin 828 -> 998 bytes .../test/partner_response_dto_test.dart | Bin 0 -> 2050 bytes .../openapi/test/update_partner_dto_test.dart | Bin 0 -> 574 bytes server/immich-openapi-specs.json | 152 +++++++++- server/src/domain/access/access.core.ts | 5 + server/src/domain/asset/asset.service.spec.ts | 78 +++++- server/src/domain/asset/asset.service.ts | 41 ++- .../src/domain/asset/dto/time-bucket.dto.ts | 5 + server/src/domain/partner/partner.dto.ts | 11 + .../domain/partner/partner.service.spec.ts | 13 +- server/src/domain/partner/partner.service.ts | 36 ++- .../domain/repositories/access.repository.ts | 4 + .../domain/repositories/asset.repository.ts | 2 +- .../domain/repositories/partner.repository.ts | 1 + .../immich/controllers/partner.controller.ts | 18 +- server/src/infra/entities/partner.entity.ts | 5 +- ...562570201-AdddInTimelineToPartnersTable.ts | 14 + .../infra/repositories/access.repository.ts | 11 + .../infra/repositories/asset.repository.ts | 8 +- .../infra/repositories/partner.repository.ts | 18 +- server/test/e2e/asset.e2e-spec.ts | 46 +++ server/test/e2e/partner.e2e-spec.ts | 20 ++ server/test/fixtures/partner.stub.ts | 2 + .../repositories/access.repository.mock.ts | 5 + .../repositories/partner.repository.mock.ts | 1 + web/src/api/open-api/api.ts | 264 +++++++++++++++++- .../components/album-page/album-viewer.svelte | 8 +- .../photos-page/actions/archive-action.svelte | 4 +- .../actions/asset-job-actions.svelte | 6 +- .../photos-page/actions/delete-assets.svelte | 10 +- .../actions/favorite-action.svelte | 5 +- .../photos-page/actions/stack-action.svelte | 4 +- .../asset-select-control-bar.svelte | 11 +- .../partner-settings.svelte | 149 ++++++++-- .../user-settings-list.svelte | 3 +- .../(user)/partners/[userId]/+page.svelte | 2 +- web/src/routes/(user)/photos/+page.svelte | 8 +- .../(user)/user-settings/+page.server.ts | 2 - .../routes/(user)/user-settings/+page.svelte | 2 +- 59 files changed, 1161 insertions(+), 123 deletions(-) create mode 100644 mobile/openapi/doc/PartnerResponseDto.md create mode 100644 mobile/openapi/doc/UpdatePartnerDto.md create mode 100644 mobile/openapi/lib/model/partner_response_dto.dart create mode 100644 mobile/openapi/lib/model/update_partner_dto.dart create mode 100644 mobile/openapi/test/partner_response_dto_test.dart create mode 100644 mobile/openapi/test/update_partner_dto_test.dart create mode 100644 server/src/domain/partner/partner.dto.ts create mode 100644 server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 798d388d7b..496fa5441d 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -2361,6 +2361,103 @@ export interface OAuthConfigResponseDto { */ 'url'?: string; } +/** + * + * @export + * @interface PartnerResponseDto + */ +export interface PartnerResponseDto { + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'deletedAt': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'email': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'externalPath': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'firstName': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'inTimeline'?: boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'isAdmin': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'lastName': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'oauthId': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'profileImagePath': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'shouldChangePassword': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'storageLabel': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'updatedAt': string; +} /** * * @export @@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto { */ 'name'?: string; } +/** + * + * @export + * @interface UpdatePartnerDto + */ +export interface UpdatePartnerDto { + /** + * + * @type {boolean} + * @memberof UpdatePartnerDto + */ + 'inTimeline': boolean; +} /** * * @export @@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (timeBucket !== undefined) { localVarQueryParameter['timeBucket'] = timeBucket; } @@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -8556,7 +8678,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * Get all asset of a device that are in the database, ID only. @@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBucket + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBuckets + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -9603,7 +9739,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -12312,6 +12448,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updatePartner', 'id', id) + // verify required parameter 'updatePartnerDto' is not null or undefined + assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto) + const localVarPath = `/partner/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12373,7 +12568,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, /** @@ -12382,7 +12577,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { + getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath)); }, /** @@ -12394,6 +12589,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12439,6 +12643,27 @@ export interface PartnerApiRemovePartnerRequest { readonly id: string } +/** + * Request parameters for updatePartner operation in PartnerApi. + * @export + * @interface PartnerApiUpdatePartnerRequest + */ +export interface PartnerApiUpdatePartnerRequest { + /** + * + * @type {string} + * @memberof PartnerApiUpdatePartner + */ + readonly id: string + + /** + * + * @type {UpdatePartnerDto} + * @memberof PartnerApiUpdatePartner + */ + readonly updatePartnerDto: UpdatePartnerDto +} + /** * PartnerApi - object-oriented interface * @export @@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PartnerApi + */ + public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) { + return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 33d2c0f3ed..ad21037867 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -187,12 +187,15 @@ class AuthenticationNotifier extends StateNotifier { if (userResponseDto != null) { Store.put(StoreKey.deviceId, deviceId); Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); - Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); + Store.put( + StoreKey.currentUser, + User.fromUserDto(userResponseDto), + ); Store.put(StoreKey.serverUrl, serverUrl); Store.put(StoreKey.accessToken, accessToken); shouldChangePassword = userResponseDto.shouldChangePassword; - user = User.fromDto(userResponseDto); + user = User.fromUserDto(userResponseDto); retResult = true; } else { diff --git a/mobile/lib/modules/partner/services/partner.service.dart b/mobile/lib/modules/partner/services/partner.service.dart index 42fcdb4381..0605e56efb 100644 --- a/mobile/lib/modules/partner/services/partner.service.dart +++ b/mobile/lib/modules/partner/services/partner.service.dart @@ -36,7 +36,7 @@ class PartnerService { final userDtos = await _apiService.partnerApi.getPartners(direction._value); if (userDtos != null) { - return userDtos.map((u) => User.fromDto(u)).toList(); + return userDtos.map((u) => User.fromPartnerDto(u)).toList(); } } catch (e) { _log.warning("failed to get partners for direction $direction:\n$e"); diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index 5a4ee43984..dafbedd319 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -63,7 +63,10 @@ class TabNavigationObserver extends AutoRouterObserver { return; } - Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); + Store.put( + StoreKey.currentUser, + User.fromUserDto(userResponseDto), + ); ref.read(serverInfoProvider.notifier).getServerVersion(); } catch (e) { debugPrint("Error refreshing user info $e"); diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index df742a1541..59f01c7dc0 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -18,11 +18,12 @@ class User { this.isPartnerSharedWith = false, this.profileImagePath = '', this.memoryEnabled = true, + this.inTimeline = false, }); Id get isarId => fastHash(id); - User.fromDto(UserResponseDto dto) + User.fromUserDto(UserResponseDto dto) : id = dto.id, updatedAt = dto.updatedAt, email = dto.email, @@ -34,6 +35,19 @@ class User { isAdmin = dto.isAdmin, memoryEnabled = dto.memoriesEnabled; + User.fromPartnerDto(PartnerResponseDto dto) + : id = dto.id, + updatedAt = dto.updatedAt, + email = dto.email, + firstName = dto.firstName, + lastName = dto.lastName, + isPartnerSharedBy = false, + isPartnerSharedWith = false, + profileImagePath = dto.profileImagePath, + isAdmin = dto.isAdmin, + memoryEnabled = dto.memoriesEnabled, + inTimeline = dto.inTimeline; + @Index(unique: true, replace: false, type: IndexType.hash) String id; DateTime updatedAt; @@ -45,6 +59,8 @@ class User { bool isAdmin; String profileImagePath; bool? memoryEnabled; + bool? inTimeline; + @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); @Backlink(to: 'sharedUsers') @@ -62,7 +78,8 @@ class User { isPartnerSharedWith == other.isPartnerSharedWith && profileImagePath == other.profileImagePath && isAdmin == other.isAdmin && - memoryEnabled == other.memoryEnabled; + memoryEnabled == other.memoryEnabled && + inTimeline == other.inTimeline; } @override @@ -77,5 +94,6 @@ class User { isPartnerSharedWith.hashCode ^ profileImagePath.hashCode ^ isAdmin.hashCode ^ - memoryEnabled.hashCode; + memoryEnabled.hashCode ^ + inTimeline.hashCode; } diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 687a784c090ff2d93ca9e9a2bee120827801b4a3..af3bdadf628befc4c1ab6c419017c722d65dadcd 100644 GIT binary patch delta 656 zcmZqq!?eGLdBbxSj=Ye}+|-=RyvcrSa+A4Og*agXsgobDOH8(66`lNoMQL(1tMFu5 z*0{+(SQRGwhH+1R$<7ayQ<{7rjAim|R@uopY=V<}Soy(x8Mc7QcYyp_ws=O1$#>aG zCm&-MoUB?UIk|@2i^QY?k6`TpVpY`LKW{ zqsipA0+y4r1%)PC2pTh*P0kTC1)8%6NLoz3A!r3OMOerbWQwOy8{^~v5x&V9!h(}u z2rEoJFDyCPKqO`Ic@d$>aUxQaH;c$jo**ng`4f;nFQPozQWV5zn=B%#3e-Q@P(*sN zxe)(kbz$Dkg`yJn2p4%4Pu9;Vo4h((k}DJL%*h*FeUQ0YIn$B3XtL_L^Eu&qCkI+9 zOwJ2)-P{$j#2zUK4VMKYV)Ek*Z!S-SZ4W1eAsMi6rq5>GDM5U=HM>r3SipwQ05_0= t&2@{ykqw!+H4NDkA2&L0zP7=VWAgjmqR8TcdlHbj6L&gnR^54@1pu36>@olV delta 260 zcmdnr!`$+RX~T1t$sw%DlX+Q%CU>&NPcCN_p8T6NYw`flaF_@8NcpiO@67#wfriKW|J53 z8!(zrzRYjVXfc^vz-BU+0Qcr#0cpm`3k4MzjVB)xv}81y{72Ab@^3+*$w5LOE#*R{ zKrL&9tbkfx0NIwC<%QcACqEI9oP0z?VX~~K#^k>uDU&}4%TMkVm6`lNgas(ZH`!7| zaPkQt|AR2wWKA*E$?Re>lX-;sH&=?> _getAllUsers({required bool isAll}) async { try { final dto = await _apiService.userApi.getAllUsers(isAll); - return dto?.map(User.fromDto).toList(); + return dto?.map(User.fromUserDto).toList(); } catch (e) { _log.warning("Failed get all users:\n$e"); return null; diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 57854e1b79..c4f9679768 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -91,6 +91,7 @@ doc/OAuthCallbackDto.md doc/OAuthConfigDto.md doc/OAuthConfigResponseDto.md doc/PartnerApi.md +doc/PartnerResponseDto.md doc/PathEntityType.md doc/PathType.md doc/PeopleResponseDto.md @@ -159,6 +160,7 @@ doc/TranscodePolicy.md doc/UpdateAlbumDto.md doc/UpdateAssetDto.md doc/UpdateLibraryDto.md +doc/UpdatePartnerDto.md doc/UpdateStackParentDto.md doc/UpdateTagDto.md doc/UpdateUserDto.md @@ -273,6 +275,7 @@ lib/model/o_auth_authorize_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart lib/model/o_auth_config_response_dto.dart +lib/model/partner_response_dto.dart lib/model/path_entity_type.dart lib/model/path_type.dart lib/model/people_response_dto.dart @@ -335,6 +338,7 @@ lib/model/transcode_policy.dart lib/model/update_album_dto.dart lib/model/update_asset_dto.dart lib/model/update_library_dto.dart +lib/model/update_partner_dto.dart lib/model/update_stack_parent_dto.dart lib/model/update_tag_dto.dart lib/model/update_user_dto.dart @@ -432,6 +436,7 @@ test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart test/o_auth_config_response_dto_test.dart test/partner_api_test.dart +test/partner_response_dto_test.dart test/path_entity_type_test.dart test/path_type_test.dart test/people_response_dto_test.dart @@ -500,6 +505,7 @@ test/transcode_policy_test.dart test/update_album_dto_test.dart test/update_asset_dto_test.dart test/update_library_dto_test.dart +test/update_partner_dto_test.dart test/update_stack_parent_dto_test.dart test/update_tag_dto_test.dart test/update_user_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0bc2cc1e333bd10ee293d344c537cb4fc4aa66ee..551169a993904ddb3b329dcf44be88c034480c75 100644 GIT binary patch delta 130 zcmaF3nQ`e>#tr+`C$p*xu$C62B$iArRF@Y8vr-EZi%RlRi!>E#6tuJgLMOjd6W_dF zy-rCEEF6?tT#%nvoa$1NAFGj)pRBJR08u}AqP56oYcD=tIj8_aMJULIRH*P~^YB$n E0BRL53;+NC delta 24 gcmZ3wmGR+b#tr+`H_L0(DQ&*!#lyRqH(~}80Eo8vg avk#!mR4y&%Ea3;Vn8afB()&1s3b47NK>IkK}#zjG-UDuMjh4wkl^NX zjE7jI!OYa6pw!}m{Ji2+my&!ftym3+0t^1xjFTNWMWO1$IZw07V5+x6)^MG}TN!8( z#BQisEiEoP1x$SkD2g;PQ*=;8fKJ)`hB21KAuTg6F$Y6V!B!y@>_e!@K;;^m)(ZOi z7{ZfJ@Mk#@ TJjf delta 86 zcmdnw*l)053*#gv38v8E&9Y2~SVcpNQ;UL9iwpAeic?)m^0l;LHGopT*f%hOl&t4G T%_@eb#15`zHsfYPK^t}ePLm$) diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md new file mode 100644 index 0000000000000000000000000000000000000000..887664e89565a350d9905710362c086b13b301ee GIT binary patch literal 959 zcmb7DO=}x55WVYH2z*E^*v-8;xwQ|5B$zZeWANA-XGJBAkjAuy{P>L4Ub1O%+AIsZ zqj~Smhh`?45=Ka_4P?=`E65x%KVnG~3(lF8noRT^fz{lh?om3y{s%mx+HSX22+D;- znf2BA{L5MMrs`I})oq3LN~EYuH0tU)* zV~Yqok3%|Rk@vv}j@&H=++STunGVk5GRO#h)S1%J(c5nw#EL8BC*?tN99s$~Oj*S4 zV22m);+X8GhC!{@UuDcSlfJL3p}i}-*<82n&EsM=zhB7UOf2n>0V6{l+vY&I=@+C- d^&5M(TK*?j)}_H{6S>ImBu)YU2tHLxy#mrKBt-xK literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/UpdatePartnerDto.md b/mobile/openapi/doc/UpdatePartnerDto.md new file mode 100644 index 0000000000000000000000000000000000000000..b336c419eb91a5e8684b3d365985362cb32150aa GIT binary patch literal 414 zcma)&O-lnY5QgvbD+cyZ8%TQBQ)PP)Y+0n9N@3$>s=-YrWXFTxk2hHhRxg@M@=l(4 z=c7SR6m9Y*u%ppDCTE2@nC>8HV}T`{yo*JrD@1MiRW0yfS9>uvxd;jR~%CJiG;pVryUe#97Z9n3njR az3%S*nOpXuDZ0cy$`6aLg1?F{0pJq`T7Q!O literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 7e4ed3a7b049e7ec63b4c8f02de44f5c8b996d69..ed0b51f88d3b24536ab2b4d776c605a8322db996 100644 GIT binary patch delta 41 wcmX?RHN|?vWdY`*lDx?iq?I>c6cFZOFGvJ(Q;Q})WRaMhFU7w3iBu9309RuVmjD0& delta 17 ZcmbPYeavdZWr5AV1q8V^8%ZZI0RTop2A}`{ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 0d3c2bfe89c8b65e77b7ced1238b7a7ecddd08b0..429a320c268649c5ee17bb871dbc57a274ac827e 100644 GIT binary patch delta 426 zcmX?qj`{E<<_!~SCl@%fah7M6WCSD@mE=thoGml?Kmy<73*F+A_ttI_PRh^Eu~&f0 zrxq3KOg0pY*?g+*GV^4ECJqj;HlXli#@*)9$m-N}P_%9CZ~DfB!!n1-tdkeC9v8y2 zZ1M+Ri^+4_4xpH9g<`PI<_3kAjGNz8i8F4F>55~-VaJ8;?YKNM`BWYE=C?g(nR$@i iPtZdvCN$yp(0<&W;hJneWi>$$O@3EZwb^Gna{vG-z^@4a delta 133 zcmX?niTV6F<_!~SCx4I^oBY9(ck-_uuE`H-H%&GYi{8vwf0=o5nvVMB?B;_^U=@3| zNlvb7+dTP`ui0eYX!E5H-c@&CF2c zKe)~^L70<|^J+i^Kk`PiOkTt;$rKPe`5BeFD^r}Xf-R=pLjGk!>WKvfIho0cC7Jno`dP*K zdFqqzaaw>JC&9IA@_If&G8`x-g&9Wnb`bj~CkhG^t?YOG+tPo5_zA&BAj&4#R6 OgrZ_{F}prfEf)aHoS$a^ delta 71 zcmca>bWUZ%F&3uK;?1X7CNoKd7N-^kr4|?D=M|^Al;o@HXeuNYD*$C{*qfO_s#v(s UG6Cty*LXERTxPy#7N%M*0JO0e*8l(j diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index dfdd5b4efecb24f0f574264992fb5409319c6cba..c03a469ae36ee5c72379fb064671b1ebd585090f 100644 GIT binary patch delta 69 zcmX@ThVlDa#tjEeSc^*XQi~=F`pUBgBm&u!FZ$?i-fxmE#t!C!q?E)bFYsoAN(*SJ OPB!pj+pOiS$PECx_ZYVT delta 19 bcmeyqmhto&#tjEeHcOgjifz8;t;h`kUYrO+ diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..ae6e230982f5aca82c7b1d78eaedb4d28a22f5f2 GIT binary patch literal 7973 zcmeHMTW{Pp7Jm1yI4%aAF;*F;eJBdIvKz$C!rjCU5_f^ZFb1^5@vzlMvmz&6BlUmp zIlPPF*lB@%TLZQw@?3as9}lU+!@a{pc=gNG<%@rwU7US*b#-xe97nfJ? z?(+2f&9D1NMwCC*tXJi?^KZW0qgQd)c0FG#yMD104L^cyyOq3N@P@B>s|xon)|;+Z zSq|3Rvj@4@Y}KOY|JjNj*;}^8zZ=%$f5m%=!h4x#uew$$)~bc5QK480-fMQ5Rn~}> z%SF?*cSxpItX+Kna#5{VFZcFPoq}!&-G;YpBj)(~!`@!iFe%|Z&b7S%z~u(@@l(~o z=RJT!bioI%wtXvsLCf#qdQ5o(LNBr6b7UFM&qxx0Gal4R$a&Rsrg;5I6`9Ez-Me#= zubF6aT>QW2%9b_nnOf!MwT~~*)Y^zr1*{Mz6QQn=e*&W|ymt1w*rhbjVCK@U4yFM#Rs;);dZ`m!+ z`ft0bLr5MEFgE~BafEyUK3;?26Y!zZ;~!No+B?u8iUv*~j!iTHJ|^*?sP!8rAFhB> z@O{IXN#>0wz;&97(i$v{7VLe-(<^;RlavZjGsR{aiGWVmjUjkRT$4C-N{>xk@rjkSnn-vg^_ zLzYpF0sn1cO^Dm=>6|I6ht`KYG>NUY|d8{+d zw|CUlwY=g|vi^a(GHkR~TneelW`66NI|&Ey4>J{V4p%@B4kz#ujzTW(k9l+&W`^U; z8%m2e&x}Byi({dp&WwfZ8>$JLK}Lz)6~^LR8jfeuv>R$mW-}^nHjV|S$v39R9!B#ig`0?E*1dbcn!9nv-c$g_CF49E`DCHyxCPKbxY0HUyC`O1; zPSP+IS$HTXxnLq~6g;Gf-tbW7I@xmKA8)2QLhsn-M-vcq@dKX2M0JdP($BJMF+%2~ zvJ`lVfZuQkN*Gape*D2~RGQ+tpZcIpZ;)O${eEb!x?fZ^JHY?vvittKm*2Q-Fhax< z^d>p?F@}AEByKU_)Od=Z#t-d3r{@uPgB`T74}mW)ln~6U>v3qJ&bOdfza|F6*|*b% zm?#n#C9YM!W4R;#oh$Y^AJq6Eqr>|^)PaVM9@K>RxP;=^X;dJI_J;!SgTN`2cHcFxOua zgp%gaEMeJqYb)(qpQG`bXo&<PdKDVmE!LaWIc%jWH)ni2lHiiuIN&x*5Kf@)OAHOXe!J2K{S!+f+O#$&nQowY2uy1adh}8pqp3zTF zHDln@K&^THO;;J0%PYKHutpqvy$0Q5eKF2H&mO_|^dKnrc>AmbPdPAJ)Xy>^bjQmM zXgcDgQ){p|5XK9(Ap0DzLEeF4hq5D9v)c#7j&Q)8z~e~BaP0|oY=A||k$`j+4q{{9 zrKtI=kd$FyQVvE(5{Gu>ScWnq@EplXO&s87l$0JCwS-X#`e{>1z67z%sSZtBWGmev zvPMmdL2m_u#25mOp}~na2#EtQGKK$H->BU6-96n|Vhf2kgpwDJ4vh>J^<2`GBsQcO z@z*zDk0POT#X?KLYX}YQb0IG}b++E92PYLV4zF)!R(N+$*NDU#<{b(ege)`oIwz3V z7UANpi^xp@S4Yh(O&e3@c&Ta7H{x`Cr};l{x8+hD88&n7Is*5krNps_Tm6jB2!gW_ zaUL+PL8|x^kP+k#1Z1^yKI(qb_nNLWvDvk;;7%MiL-(m@ffxMvQHnl3nd^x`9C5UN zaFLwT8-96k_K@q1Ovx&}m5Xo7^PzPlOj)qZWjMLydy= zQ)ez1?}b|75LFU=#u`WBg&{;f+l9~tINs*dcQ2F|I)!)9A&{~uS5n8sLX+7XDd5Yh z7G=cQvs2mM$)PVyk1t6z+o42=@N6cbWy4U9<1wdDdG|#LRrsO}%P)|l>8U*`osS5XYG*a zWN*5fY1C*L3e!5-*|=+(+Og2u*a2)=+>>o2?5l4L?7Q2f?dxc}wd2`s)6GRYrqdej MaA*SFWguk!38Bvo!TY)i6^3_uWfM zvixY0&46u*y3cdYB{doiMk9FrWj1~Gc5*xU^m;bAhMRXECLvr;;dVNMkJIaSH-Dd@ z8Ckx{n6~kY=+_qmx)n>QG*5D+lU$Vi5^7l+o+Z5GD=ux=-iuYGv_0v;$~D`Vq^fPA z`9HPL=q}j`e=DZ(-*Rm*xHhNVQzeaM(k3Fqgd!JQJ9l$3St%qpNvY%#&1}JB^4s$y zEtocg0j9H{a!?hQtP&A^uLgs(WX8akDr1)ai58OUYpdXX05C!TK5<)XX@G&`YnUI} zEkL;PQXZkh;nRSk0JLp`Erf}LoQV~uLPLDIy}%j(?vyGqRQtrOy;9$~*4TFY91GXX zDh+WQOz*z(6x*={*B20v!NZfXGN3GB`{c(De+A-1ljrBIK5x-03==$5jF1+V+(N-j z@kV7F)4MpGUPvaAr`#B(H$*csS8yYYGYfWoMr{a>LcinBRtvcVtNcaanj9V?Jm1BN zf7x9xh7Q4xta|C)L&!ELI+2bs;(Mbc#`EOlLb4LR;u6GgWPQKA@(?+TxgPFv3>WBd zz^FIYPBYGf7Pdt2f6@{#nSmcE1Hu~k&dQn_$hBHEvF2_G<(H$ukn}mnZWkfQx-31R zYhj!!d$d%V=QtQ{rgWX6mP|q_OK`-jNE4jKLS;mgUpP|q@2&<1U<0VHtwj^d30!5Q zH!P@K2iU{enM5fw_6YI7OTo_#&E8QB0hZ@KJ2Ea|#j3vurD;CfRQx3k)HonoL)c^O z;*7?QKA4F9w(hs#jL_>iYcTjAu*^f@jiJQl8zf{844?4cjq_5gHM=WuJRoPK;o-JL z!(*YjsY^?$I|@Eu_}U0g3Q1^E8E}5?WEBZ>tL$V|*-aZ05yqV%XhN$s4zM%bJ#RVi z{KQANMkFuS4Hn*jLjdhKJ&T>Z`XOm|qc$RzWuEz-r-!zMcn|2t%PMLsXra1<_R2+Z zlhd%!pXDRGi2ajXw557j(5gS=M`7A&@57FHA4J{J6MI&ECTN`g<|Ms@?$Ar{+`;3O zj#0la?lFj~ded?k(VgI??S@$K9X-ya3lurK9y5<{ia^c1<2Sycea`uNO;Yj)h(*_K zfUs@6rOnBqr2Lt+7Pi18njboj4_SET&Csja_i)EXb9yhKXgIAW6hSIF9-}ci(4&*u z2;pgSiZS}C`AMq(qn#(p;wchq_O#v}@Th{YuYHmanTUv@4!WL@!fs4=0~>bg^5WQ= zc1+b1#Rz5v@A&WDj!rU4tQJ{OiN?H;Qi<6Exlwl?7K6pd$sz5e7o~| ReDC1sAJ*<&>uP{I%D)Wrnh*d0 literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index e241c34ca9651083fb23e2d870a3502e43b8516c..ec7a2182d1cb5c9009d67257351408cc5b774109 100644 GIT binary patch delta 64 xcmZ3bcR_E1E1zIeetwQZd1gsQKw?ozUTRVClMS8$nbQ)1%1`nAre^gbDfp diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..14092a44f60f514422cc7a48457cc67554423375 GIT binary patch literal 2050 zcmbW1O>f&U42JLi72GK)kk(sILyH0n=5z?q#YN{Gc5)SqiG|6MC&}xAq5pl9k{E4g zz_Abh5b*MSNQq(`M{x}E?^$~OWwBa3%(F!Tx68Z543ZR9X$D`@WO@7J9m%}%6KBTV zTwZ**jQmn;l{T1fw8=L@;uSQivB-0jSfO&W<62a;Hm);*$tSk6dEL0&;IBp)3RkS6 zcg+la9U4pK#&(AnT3N@G%LON(*a$Qs+;+mGP-t_h)s|$gWGesoJ}*jUY!vNtgh|bc zpKOa)x<|aNLhx)Yz+7h^@D23($ZB1EX;&k*|3l0?%YI+8L zBLLEGgklmzD5F5Q=?I&iT}LF|8r{^>i3fCuCNnsg2HS)=%tVDy7^Gi5cjN#eJF?K> z2V99z8>-Z}9Xz(m$GsP`J5_`11e30>4Yu~fJ!Tg=-w{N8*Fm}!> zq52tn-5|iBrzErPq+>V=2SLJSRaj{w&@L2POYA?8H~K-qLsgnJu3Yw`4!SW28XkV| zFDsguC*7F00fV4nBW8KcW0Nz-L|$}4Ke(8r&Zzs4sBl^>mY*%@v9MTKa;#KCdXdv5WgmzLy8baWeG>GfzGnOk>R zUTWpMP@c<*2FhBZjdFK!SSuAe-fGnl=J!JJyBl8ag>{VmW`x9u@?A7I)drO?GNm&P zTPL$K#G~_RxK2PA8hQuxC9pM>@c2wxg*Nk=eQH@jC!`t-;hO;vT}vfeka39;VSXkW z3}=kc#_HY-l3;#@BqR7UjFxX&2ggEo81^MG#89Nxu>ikEFqRgy{s6mwboc8d+(DHw UV31PCBG2+@a|~gqitmyA0KNphNdN!< literal 0 HcmV?d00001 diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 193586cbd4..782b69649a 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2071,6 +2071,14 @@ "type": "boolean" } }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "timeBucket", "required": true, @@ -2199,6 +2207,14 @@ "type": "boolean" } }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "key", "required": false, @@ -3491,7 +3507,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/UserResponseDto" + "$ref": "#/components/schemas/PartnerResponseDto" }, "type": "array" } @@ -3568,7 +3584,57 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserResponseDto" + "$ref": "#/components/schemas/PartnerResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Partner" + ] + }, + "put": { + "operationId": "updatePartner", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerResponseDto" } } }, @@ -7572,6 +7638,77 @@ ], "type": "object" }, + "PartnerResponseDto": { + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "deletedAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "type": "string" + }, + "externalPath": { + "nullable": true, + "type": "string" + }, + "firstName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "inTimeline": { + "type": "boolean" + }, + "isAdmin": { + "type": "boolean" + }, + "lastName": { + "type": "string" + }, + "memoriesEnabled": { + "type": "boolean" + }, + "oauthId": { + "type": "string" + }, + "profileImagePath": { + "type": "string" + }, + "shouldChangePassword": { + "type": "boolean" + }, + "storageLabel": { + "nullable": true, + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "firstName", + "lastName", + "email", + "profileImagePath", + "storageLabel", + "externalPath", + "shouldChangePassword", + "isAdmin", + "createdAt", + "deletedAt", + "updatedAt", + "oauthId" + ], + "type": "object" + }, "PathEntityType": { "enum": [ "asset", @@ -8982,6 +9119,17 @@ }, "type": "object" }, + "UpdatePartnerDto": { + "properties": { + "inTimeline": { + "type": "boolean" + } + }, + "required": [ + "inTimeline" + ], + "type": "object" + }, "UpdateStackParentDto": { "properties": { "newParentId": { diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 88abd79b19..16527de989 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -40,6 +40,8 @@ export enum Permission { PERSON_READ = 'person.read', PERSON_WRITE = 'person.write', PERSON_MERGE = 'person.merge', + + PARTNER_UPDATE = 'partner.update', } let instance: AccessCore | null; @@ -242,6 +244,9 @@ export class AccessCore { case Permission.PERSON_MERGE: return this.repository.person.hasOwnerAccess(authUser.id, id); + case Permission.PARTNER_UPDATE: + return this.repository.partner.hasUpdateAccess(authUser.id, id); + default: return false; } diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 7b93935f62..45687282f8 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -10,6 +10,7 @@ import { newCommunicationRepositoryMock, newCryptoRepositoryMock, newJobRepositoryMock, + newPartnerRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, } from '@test'; @@ -23,6 +24,7 @@ import { ICommunicationRepository, ICryptoRepository, IJobRepository, + IPartnerRepository, IStorageRepository, ISystemConfigRepository, JobItem, @@ -164,6 +166,7 @@ describe(AssetService.name, () => { let storageMock: jest.Mocked; let communicationMock: jest.Mocked; let configMock: jest.Mocked; + let partnerMock: jest.Mocked; it('should work', () => { expect(sut).toBeDefined(); @@ -177,7 +180,18 @@ describe(AssetService.name, () => { jobMock = newJobRepositoryMock(); storageMock = newStorageRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, configMock, storageMock, communicationMock); + partnerMock = newPartnerRepositoryMock(); + + sut = new AssetService( + accessMock, + assetMock, + cryptoMock, + jobMock, + configMock, + storageMock, + communicationMock, + partnerMock, + ); when(assetMock.getById) .calledWith(assetStub.livePhotoStillAsset.id) @@ -327,7 +341,7 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, }), ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); - expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userId: authStub.admin.id }); + expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.id] }); }); }); @@ -363,7 +377,7 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, timeBucket: 'bucket', isArchived: true, - userId: authStub.admin.id, + userIds: [authStub.admin.id], }); }); @@ -380,9 +394,65 @@ describe(AssetService.name, () => { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', - userId: authStub.admin.id, + userIds: [authStub.admin.id], }); }); + + it('should throw an error if withParners is true and isArchived true or undefined', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: undefined, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); + + it('should throw an error if withParners is true and isFavorite is either true or false', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: false, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); + + it('should throw an error if withParners is true and isTrash is true', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isTrashed: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); }); describe('downloadFile', () => { diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 34da786806..3b9b56412f 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -16,9 +16,11 @@ import { ICommunicationRepository, ICryptoRepository, IJobRepository, + IPartnerRepository, IStorageRepository, ISystemConfigRepository, ImmichReadStream, + TimeBucketOptions, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; import { SystemConfigCore } from '../system-config'; @@ -83,6 +85,7 @@ export class AssetService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, ) { this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(configRepository); @@ -187,11 +190,25 @@ export class AssetService { await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]); } } + + if (dto.withPartners) { + const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; + const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; + const requestedTrash = dto.isTrashed === true; + + if (requestedArchived || requestedFavorite || requestedTrash) { + throw new BadRequestException( + 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + ); + } + } } async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(authUser, dto); - return this.assetRepository.getTimeBuckets(dto); + const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + + return this.assetRepository.getTimeBuckets(timeBucketOptions); } async getTimeBucket( @@ -199,7 +216,8 @@ export class AssetService { dto: TimeBucketAssetDto, ): Promise { await this.timeBucketChecks(authUser, dto); - const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); if (authUser.isShowMetadata) { return assets.map((asset) => mapAsset(asset, { withStack: true })); } else { @@ -207,6 +225,25 @@ export class AssetService { } } + async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise { + const { userId, ...options } = dto; + let userIds: string[] | undefined = undefined; + + if (userId) { + userIds = [userId]; + + if (dto.withPartners) { + const partners = await this.partnerRepository.getAll(authUser.id); + const partnersIds = partners + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) + .map((partner) => partner.sharedById); + + userIds.push(...partnersIds); + } + } + + return { ...options, userIds }; + } async downloadFile(authUser: AuthUserDto, id: string): Promise { await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id); diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index db2f1ecd0b..849b8713f0 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -38,6 +38,11 @@ export class TimeBucketDto { @IsBoolean() @Transform(toBoolean) withStacked?: boolean; + + @Optional() + @IsBoolean() + @Transform(toBoolean) + withPartners?: boolean; } export class TimeBucketAssetDto extends TimeBucketDto { diff --git a/server/src/domain/partner/partner.dto.ts b/server/src/domain/partner/partner.dto.ts new file mode 100644 index 0000000000..17afcad5d0 --- /dev/null +++ b/server/src/domain/partner/partner.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty } from 'class-validator'; +import { UserResponseDto } from '../user'; + +export class UpdatePartnerDto { + @IsNotEmpty() + inTimeline!: boolean; +} + +export class PartnerResponseDto extends UserResponseDto { + inTimeline?: boolean; +} diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 2bc561194d..0bae95aa79 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -1,11 +1,11 @@ import { BadRequestException } from '@nestjs/common'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; -import { UserResponseDto } from '../index'; -import { IPartnerRepository, PartnerDirection } from '../repositories'; +import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories'; +import { PartnerResponseDto } from './partner.dto'; import { PartnerService } from './partner.service'; const responseDto = { - admin: { + admin: { email: 'admin@test.com', firstName: 'admin_first_name', id: 'admin_id', @@ -20,8 +20,9 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + inTimeline: true, }, - user1: { + user1: { email: 'immich@test.com', firstName: 'immich_first_name', id: 'user-id', @@ -36,16 +37,18 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + inTimeline: true, }, }; describe(PartnerService.name, () => { let sut: PartnerService; let partnerMock: jest.Mocked; + let accessMock: jest.Mocked; beforeEach(async () => { partnerMock = newPartnerRepositoryMock(); - sut = new PartnerService(partnerMock); + sut = new PartnerService(partnerMock, accessMock); }); it('should work', () => { diff --git a/server/src/domain/partner/partner.service.ts b/server/src/domain/partner/partner.service.ts index 797938a8d1..93600a5c0d 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/domain/partner/partner.service.ts @@ -1,14 +1,22 @@ import { PartnerEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AccessCore, Permission } from '../access'; import { AuthUserDto } from '../auth'; -import { IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; -import { UserResponseDto, mapUser } from '../user'; +import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; +import { mapUser } from '../user'; +import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto'; @Injectable() export class PartnerService { - constructor(@Inject(IPartnerRepository) private repository: IPartnerRepository) {} + private access: AccessCore; + constructor( + @Inject(IPartnerRepository) private repository: IPartnerRepository, + @Inject(IAccessRepository) accessRepository: IAccessRepository, + ) { + this.access = AccessCore.create(accessRepository); + } - async create(authUser: AuthUserDto, sharedWithId: string): Promise { + async create(authUser: AuthUserDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; const exists = await this.repository.get(partnerId); if (exists) { @@ -29,7 +37,7 @@ export class PartnerService { await this.repository.remove(partner); } - async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { + async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { const partners = await this.repository.getAll(authUser.id); const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; return partners @@ -38,8 +46,22 @@ export class PartnerService { .map((partner) => this.map(partner, direction)); } - private map(partner: PartnerEntity, direction: PartnerDirection): UserResponseDto { + async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise { + await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById); + const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id }; + + const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); + return this.map(entity, PartnerDirection.SharedWith); + } + + private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto { // this is opposite to return the non-me user of the "partner" - return mapUser(direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy); + const user = mapUser( + direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy, + ) as PartnerResponseDto; + + user.inTimeline = partner.inTimeline; + + return user; } } diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index f9ceb6f520..9c009719d3 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -35,4 +35,8 @@ export interface IAccessRepository { person: { hasOwnerAccess(userId: string, personId: string): Promise; }; + + partner: { + hasUpdateAccess(userId: string, partnerId: string): Promise; + }; } diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index da8f8547eb..2ceabad351 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -65,7 +65,7 @@ export interface TimeBucketOptions { isTrashed?: boolean; albumId?: string; personId?: string; - userId?: string; + userIds?: string[]; withStacked?: boolean; } diff --git a/server/src/domain/repositories/partner.repository.ts b/server/src/domain/repositories/partner.repository.ts index fd436932cb..f0409b67ac 100644 --- a/server/src/domain/repositories/partner.repository.ts +++ b/server/src/domain/repositories/partner.repository.ts @@ -17,4 +17,5 @@ export interface IPartnerRepository { get(partner: PartnerIds): Promise; create(partner: PartnerIds): Promise; remove(entity: PartnerEntity): Promise; + update(entity: Partial): Promise; } diff --git a/server/src/immich/controllers/partner.controller.ts b/server/src/immich/controllers/partner.controller.ts index 0ecec54cd5..5f9f004f90 100644 --- a/server/src/immich/controllers/partner.controller.ts +++ b/server/src/immich/controllers/partner.controller.ts @@ -1,5 +1,6 @@ -import { AuthUserDto, PartnerDirection, PartnerService, UserResponseDto } from '@app/domain'; -import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; +import { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain'; +import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto'; +import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { AuthUser, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -17,15 +18,24 @@ export class PartnerController { getPartners( @AuthUser() authUser: AuthUserDto, @Query('direction') direction: PartnerDirection, - ): Promise { + ): Promise { return this.service.getAll(authUser, direction); } @Post(':id') - createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { + createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(authUser, id); } + @Put(':id') + updatePartner( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UpdatePartnerDto, + ): Promise { + return this.service.update(authUser, id, dto); + } + @Delete(':id') removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(authUser, id); diff --git a/server/src/infra/entities/partner.entity.ts b/server/src/infra/entities/partner.entity.ts index d7be830828..35d32e4c9b 100644 --- a/server/src/infra/entities/partner.entity.ts +++ b/server/src/infra/entities/partner.entity.ts @@ -1,4 +1,4 @@ -import { CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; import { UserEntity } from './user.entity'; @@ -23,4 +23,7 @@ export class PartnerEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + + @Column({ type: 'boolean', default: false }) + inTimeline!: boolean; } diff --git a/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts b/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts new file mode 100644 index 0000000000..59c2f229eb --- /dev/null +++ b/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AdddInTimelineToPartnersTable1699562570201 implements MigrationInterface { + name = 'AdddInTimelineToPartnersTable1699562570201' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "partners" ADD "inTimeline" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "partners" DROP COLUMN "inTimeline"`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index aff498ac34..fa58628851 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -249,4 +249,15 @@ export class AccessRepository implements IAccessRepository { }); }, }; + + partner = { + hasUpdateAccess: (userId: string, partnerId: string): Promise => { + return this.partnerRepository.exist({ + where: { + sharedById: partnerId, + sharedWithId: userId, + }, + }); + }, + }; } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 5112fc9f65..3f0b439a37 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -519,7 +519,7 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: TimeBucketOptions) { - const { isArchived, isFavorite, isTrashed, albumId, personId, userId, withStacked } = options; + const { isArchived, isFavorite, isTrashed, albumId, personId, userIds, withStacked } = options; let builder = this.repository .createQueryBuilder('asset') @@ -532,11 +532,11 @@ export class AssetRepository implements IAssetRepository { builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId }); } - if (userId) { - builder = builder.andWhere('asset.ownerId = :userId', { userId }); + if (userIds) { + builder = builder.andWhere('asset.ownerId IN (:...userIds )', { userIds }); } - if (isArchived != undefined) { + if (isArchived !== undefined) { builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived }); } diff --git a/server/src/infra/repositories/partner.repository.ts b/server/src/infra/repositories/partner.repository.ts index c56d8b0757..b5b876558c 100644 --- a/server/src/infra/repositories/partner.repository.ts +++ b/server/src/infra/repositories/partner.repository.ts @@ -1,7 +1,7 @@ import { IPartnerRepository, PartnerIds } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { DeepPartial, Repository } from 'typeorm'; import { PartnerEntity } from '../entities'; @Injectable() @@ -16,12 +16,22 @@ export class PartnerRepository implements IPartnerRepository { return this.repository.findOne({ where: { sharedById, sharedWithId } }); } - async create({ sharedById, sharedWithId }: PartnerIds): Promise { - await this.repository.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); - return this.repository.findOneOrFail({ where: { sharedById, sharedWithId } }); + create({ sharedById, sharedWithId }: PartnerIds): Promise { + return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); } async remove(entity: PartnerEntity): Promise { await this.repository.remove(entity); } + + update(entity: Partial): Promise { + return this.save(entity); + } + + private async save(entity: DeepPartial): Promise { + await this.repository.save(entity); + return this.repository.findOneOrFail({ + where: { sharedById: entity.sharedById, sharedWithId: entity.sharedWithId }, + }); + } } diff --git a/server/test/e2e/asset.e2e-spec.ts b/server/test/e2e/asset.e2e-spec.ts index 7736f085fe..70be784c75 100644 --- a/server/test/e2e/asset.e2e-spec.ts +++ b/server/test/e2e/asset.e2e-spec.ts @@ -584,6 +584,52 @@ describe(`${AssetController.name} (e2e)`, () => { ]), ); }); + + it('should return error if time bucket is requested with partners asset and archived', async () => { + const req1 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorStub.badRequest()); + + const req2 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: undefined }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorStub.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and favorite', async () => { + const req1 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorStub.badRequest()); + + const req2 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: false }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorStub.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and trash', async () => { + const req = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isTrashed: true }); + + expect(req.status).toBe(400); + expect(req.body).toEqual(errorStub.badRequest()); + }); }); describe('GET /asset/map-marker', () => { diff --git a/server/test/e2e/partner.e2e-spec.ts b/server/test/e2e/partner.e2e-spec.ts index 82a09dcc8d..f6b8e144fe 100644 --- a/server/test/e2e/partner.e2e-spec.ts +++ b/server/test/e2e/partner.e2e-spec.ts @@ -115,6 +115,26 @@ describe(`${PartnerController.name} (e2e)`, () => { }); }); + describe('PUT /partner/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).put(`/partner/${user2.userId}`); + + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should update partner', async () => { + await repository.create({ sharedById: user2.userId, sharedWithId: user1.userId }); + const { status, body } = await request(server) + .put(`/partner/${user2.userId}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ inTimeline: false }); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false })); + }); + }); + describe('DELETE /partner/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server).delete(`/partner/${user2.userId}`); diff --git a/server/test/fixtures/partner.stub.ts b/server/test/fixtures/partner.stub.ts index dc54332ead..05c1c67d6e 100644 --- a/server/test/fixtures/partner.stub.ts +++ b/server/test/fixtures/partner.stub.ts @@ -9,6 +9,7 @@ export const partnerStub = { sharedBy: userStub.admin, sharedWith: userStub.user1, sharedWithId: userStub.user1.id, + inTimeline: true, }), user1ToAdmin1: Object.freeze({ createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -17,5 +18,6 @@ export const partnerStub = { sharedById: userStub.user1.id, sharedWithId: userStub.admin.id, sharedWith: userStub.admin, + inTimeline: true, }), }; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 6abfc7c9e1..8f1e9355d8 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -8,6 +8,7 @@ export interface IAccessRepositoryMock { library: jest.Mocked; timeline: jest.Mocked; person: jest.Mocked; + partner: jest.Mocked; } export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { @@ -50,5 +51,9 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => person: { hasOwnerAccess: jest.fn(), }, + + partner: { + hasUpdateAccess: jest.fn(), + }, }; }; diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts index a283325d29..1e839ae4f7 100644 --- a/server/test/repositories/partner.repository.mock.ts +++ b/server/test/repositories/partner.repository.mock.ts @@ -6,5 +6,6 @@ export const newPartnerRepositoryMock = (): jest.Mocked => { remove: jest.fn(), getAll: jest.fn(), get: jest.fn(), + update: jest.fn(), }; }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 798d388d7b..496fa5441d 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -2361,6 +2361,103 @@ export interface OAuthConfigResponseDto { */ 'url'?: string; } +/** + * + * @export + * @interface PartnerResponseDto + */ +export interface PartnerResponseDto { + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'deletedAt': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'email': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'externalPath': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'firstName': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'inTimeline'?: boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'isAdmin': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'lastName': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'oauthId': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'profileImagePath': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'shouldChangePassword': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'storageLabel': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'updatedAt': string; +} /** * * @export @@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto { */ 'name'?: string; } +/** + * + * @export + * @interface UpdatePartnerDto + */ +export interface UpdatePartnerDto { + /** + * + * @type {boolean} + * @memberof UpdatePartnerDto + */ + 'inTimeline': boolean; +} /** * * @export @@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (timeBucket !== undefined) { localVarQueryParameter['timeBucket'] = timeBucket; } @@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -8556,7 +8678,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * Get all asset of a device that are in the database, ID only. @@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBucket + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBuckets + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -9603,7 +9739,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -12312,6 +12448,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updatePartner', 'id', id) + // verify required parameter 'updatePartnerDto' is not null or undefined + assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto) + const localVarPath = `/partner/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12373,7 +12568,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, /** @@ -12382,7 +12577,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { + getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath)); }, /** @@ -12394,6 +12589,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12439,6 +12643,27 @@ export interface PartnerApiRemovePartnerRequest { readonly id: string } +/** + * Request parameters for updatePartner operation in PartnerApi. + * @export + * @interface PartnerApiUpdatePartnerRequest + */ +export interface PartnerApiUpdatePartnerRequest { + /** + * + * @type {string} + * @memberof PartnerApiUpdatePartner + */ + readonly id: string + + /** + * + * @type {UpdatePartnerDto} + * @memberof PartnerApiUpdatePartner + */ + readonly updatePartnerDto: UpdatePartnerDto +} + /** * PartnerApi - object-oriented interface * @export @@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PartnerApi + */ + public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) { + return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 2333a55f0b..5b10ab1e87 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -97,8 +97,12 @@
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if $isMultiSelectState && user} + assetInteractionStore.clearMultiselect()} + > {#if sharedLink.allowDownload} diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index dff6f908a5..a72bc106ab 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -20,14 +20,14 @@ let loading = false; - const { getAssets, clearSelect } = getAssetControlContext(); + const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleArchive = async () => { const isArchived = !unarchive; loading = true; try { - const assets = Array.from(getAssets()).filter((asset) => asset.isArchived !== isArchived); + const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isArchived !== isArchived); const ids = assets.map(({ id }) => id); if (ids.length > 0) { diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte index 3e678cdf09..296197a710 100644 --- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte +++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte @@ -14,13 +14,13 @@ AssetJobName.TranscodeVideo, ]; - const { getAssets, clearSelect } = getAssetControlContext(); + const { clearSelect, getOwnedAssets } = getAssetControlContext(); - $: isAllVideos = Array.from(getAssets()).every((asset) => asset.type === AssetTypeEnum.Video); + $: isAllVideos = Array.from(getOwnedAssets()).every((asset) => asset.type === AssetTypeEnum.Video); const handleRunJob = async (name: AssetJobName) => { try { - const ids = Array.from(getAssets()).map(({ id }) => id); + const ids = Array.from(getOwnedAssets()).map(({ id }) => id); await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } }); notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info }); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index c5461b2e0e..68d133ae97 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -17,7 +17,7 @@ export let menuItem = false; export let force = !$featureFlags.trash; - const { getAssets, clearSelect } = getAssetControlContext(); + const { clearSelect, getOwnedAssets } = getAssetControlContext(); const dispatch = createEventDispatcher(); @@ -37,7 +37,7 @@ loading = true; try { - const ids = Array.from(getAssets()) + const ids = Array.from(getOwnedAssets()) .filter((a) => !a.isExternal) .map((a) => a.id); await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } }); @@ -75,7 +75,7 @@ {#if isShowConfirmation} (isShowConfirmation = false)} @@ -84,8 +84,8 @@

Are you sure you want to permanently delete - {#if getAssets().size > 1} - these {getAssets().size} assets? This will also remove them from their album(s). + {#if getOwnedAssets().size > 1} + these {getOwnedAssets().size} assets? This will also remove them from their album(s). {:else} this asset? This will also remove it from its album(s). {/if} diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index 218a75d31e..b246670f6e 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -20,14 +20,15 @@ let loading = false; - const { getAssets, clearSelect } = getAssetControlContext(); + const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleFavorite = async () => { const isFavorite = !removeFavorite; loading = true; try { - const assets = Array.from(getAssets()).filter((asset) => asset.isFavorite !== isFavorite); + const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isFavorite !== isFavorite); + const ids = assets.map(({ id }) => id); if (ids.length > 0) { diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte index 8763797568..c622998fe4 100644 --- a/web/src/lib/components/photos-page/actions/stack-action.svelte +++ b/web/src/lib/components/photos-page/actions/stack-action.svelte @@ -10,11 +10,11 @@ export let onStack: OnStack | undefined = undefined; - const { getAssets, clearSelect } = getAssetControlContext(); + const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleStack = async () => { try { - const assets = Array.from(getAssets()); + const assets = Array.from(getOwnedAssets()); const parent = assets.at(0); if (parent == undefined) { diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index bc6fb823c9..5ce4a9ebd0 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -9,7 +9,8 @@ export interface AssetControlContext { // Wrap assets in a function, because context isn't reactive. - getAssets: () => Set; + getAssets: () => Set; // All assets includes partners' assets + getOwnedAssets: () => Set; // Only assets owned by the user clearSelect: () => void; } @@ -25,8 +26,14 @@ export let assets: Set; export let clearSelect: () => void; + export let ownerId: string | undefined = undefined; - setContext({ getAssets: () => assets, clearSelect }); + setContext({ + getAssets: () => assets, + getOwnedAssets: () => + ownerId !== undefined ? new Set(Array.from(assets).filter((asset) => asset.ownerId === ownerId)) : assets, + clearSelect, + }); diff --git a/web/src/lib/components/user-settings-page/partner-settings.svelte b/web/src/lib/components/user-settings-page/partner-settings.svelte index aabe911ef0..ee5fe8a091 100644 --- a/web/src/lib/components/user-settings-page/partner-settings.svelte +++ b/web/src/lib/components/user-settings-page/partner-settings.svelte @@ -1,22 +1,71 @@

{#if partners.length > 0} -
- {#each partners as partner (partner.id)} -
- -
-

- {partner.firstName} - {partner.lastName} -

-

- {partner.email} -

+ {#each partners as partner (partner.user.id)} +
+
+
+ +
+

+ {partner.user.firstName} + {partner.user.lastName} +

+

+ {partner.user.email} +

+
- (removePartner = partner)} - icon={mdiClose} - size={'16'} - title="Remove partner" - /> + + {#if partner.sharedByMe} + (removePartner = partner.user)} + icon={mdiClose} + size={'16'} + title="Stop sharing your photos with this user" + /> + {/if}
- {/each} -
+ +
+ + {#if partner.sharedByMe} +
+

SHARED WITH {partner.user.firstName.toUpperCase()}

+

{partner.user.firstName} can access

+
    +
  • + All your photos and videos except those in Archived and Deleted +
  • +
  • + The location where your photos were taken +
  • +
+ {/if} + + + {#if partner.sharedWithMe} +
+

PHOTOS FROM {partner.user.firstName.toUpperCase()}

+ handleShowOnTimelineChanged(partner, detail)} + /> + {/if} +
+
+ {/each} {/if} -
+ +
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index 3a90da0b3e..6bce7ac0e8 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -18,7 +18,6 @@ export let keys: APIKeyResponseDto[] = []; export let devices: AuthDeviceResponseDto[] = []; - export let partners: UserResponseDto[] = []; let oauthOpen = false; if (browser) { @@ -61,7 +60,7 @@ - + diff --git a/web/src/routes/(user)/partners/[userId]/+page.svelte b/web/src/routes/(user)/partners/[userId]/+page.svelte index a52303d835..9beec5b8d2 100644 --- a/web/src/routes/(user)/partners/[userId]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/+page.svelte @@ -16,7 +16,7 @@ export let data: PageData; - const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false }); + const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true }); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte index fb0f9f9693..7515d747b4 100644 --- a/web/src/routes/(user)/photos/+page.svelte +++ b/web/src/routes/(user)/photos/+page.svelte @@ -26,7 +26,7 @@ let { isViewing: showAssetViewer } = assetViewingStore; let handleEscapeKey = false; - const assetStore = new AssetStore({ isArchived: false, withStacked: true }); + const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true }); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; @@ -48,7 +48,11 @@ {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + assetInteractionStore.clearMultiselect()} + > (handleEscapeKey = true)} /> diff --git a/web/src/routes/(user)/user-settings/+page.server.ts b/web/src/routes/(user)/user-settings/+page.server.ts index a6aa5e15a4..3465826383 100644 --- a/web/src/routes/(user)/user-settings/+page.server.ts +++ b/web/src/routes/(user)/user-settings/+page.server.ts @@ -10,13 +10,11 @@ export const load = (async ({ parent, locals }) => { const { data: keys } = await locals.api.keyApi.getApiKeys(); const { data: devices } = await locals.api.authenticationApi.getAuthDevices(); - const { data: partners } = await locals.api.partnerApi.getPartners({ direction: 'shared-by' }); return { user, keys, devices, - partners, meta: { title: 'Settings', }, diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index d63a28e908..32d95a18dd 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -9,7 +9,7 @@
- +