diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/api/specs/partner.e2e-spec.ts index db37791bac..9047a97055 100644 --- a/e2e/src/api/specs/partner.e2e-spec.ts +++ b/e2e/src/api/specs/partner.e2e-spec.ts @@ -23,8 +23,8 @@ describe('/partners', () => { ]); await Promise.all([ - createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }), - createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }), + createPartner({ partnerCreateDto: { sharedWithId: user2.userId } }, { headers: asBearerAuth(user1.accessToken) }), + createPartner({ partnerCreateDto: { sharedWithId: user1.userId } }, { headers: asBearerAuth(user2.accessToken) }), ]); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 3ac374f166..b33d6cb190 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -462,7 +462,8 @@ export const utils = { updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) => updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), - createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }), + createPartner: (accessToken: string, id: string) => + createPartner({ partnerCreateDto: { sharedWithId: id } }, { headers: asBearerAuth(accessToken) }), updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) => updateMyPreferences({ userPreferencesUpdateDto }, { headers: asBearerAuth(accessToken) }), diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index 82554d62e8..d497da4d4c 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -22,14 +22,14 @@ class PartnerApiRepository extends ApiRepository { } Future create(String id) async { - final dto = await checkNull(_api.createPartner(id)); + final dto = await checkNull(_api.createPartnerDeprecated(id)); return UserConverter.fromPartnerDto(dto); } Future delete(String id) => _api.removePartner(id); Future update(String id, {required bool inTimeline}) async { - final dto = await checkNull(_api.updatePartner(id, UpdatePartnerDto(inTimeline: inTimeline))); + final dto = await checkNull(_api.updatePartner(id, PartnerUpdateDto(inTimeline: inTimeline))); return UserConverter.fromPartnerDto(dto); } } diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 863e6e01a6..6cd26aefc4 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -126,6 +126,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | +*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | @@ -171,7 +172,8 @@ Class | Method | HTTP request | Description *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | *OAuthApi* | [**startOAuth**](doc//OAuthApi.md#startoauth) | **POST** /oauth/authorize | *OAuthApi* | [**unlinkOAuthAccount**](doc//OAuthApi.md#unlinkoauthaccount) | **POST** /oauth/unlink | -*PartnersApi* | [**createPartner**](doc//PartnersApi.md#createpartner) | **POST** /partners/{id} | +*PartnersApi* | [**createPartner**](doc//PartnersApi.md#createpartner) | **POST** /partners | +*PartnersApi* | [**createPartnerDeprecated**](doc//PartnersApi.md#createpartnerdeprecated) | **POST** /partners/{id} | *PartnersApi* | [**getPartners**](doc//PartnersApi.md#getpartners) | **GET** /partners | *PartnersApi* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} | *PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} | @@ -416,8 +418,10 @@ Class | Method | HTTP request | Description - [OnThisDayDto](doc//OnThisDayDto.md) - [OnboardingDto](doc//OnboardingDto.md) - [OnboardingResponseDto](doc//OnboardingResponseDto.md) + - [PartnerCreateDto](doc//PartnerCreateDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) + - [PartnerUpdateDto](doc//PartnerUpdateDto.md) - [PeopleResponse](doc//PeopleResponse.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleUpdate](doc//PeopleUpdate.md) @@ -566,7 +570,6 @@ Class | Method | HTTP request | Description - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md) - [UserAdminDeleteDto](doc//UserAdminDeleteDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a197f17fa7..e87c160d96 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -190,8 +190,10 @@ part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; part 'model/onboarding_dto.dart'; part 'model/onboarding_response_dto.dart'; +part 'model/partner_create_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; +part 'model/partner_update_dto.dart'; part 'model/people_response.dart'; part 'model/people_response_dto.dart'; part 'model/people_update.dart'; @@ -340,7 +342,6 @@ part 'model/update_album_dto.dart'; part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; -part 'model/update_partner_dto.dart'; part 'model/usage_by_user_dto.dart'; part 'model/user_admin_create_dto.dart'; part 'model/user_admin_delete_dto.dart'; diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index f9a496b990..cdcd27750d 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -16,6 +16,59 @@ class DeprecatedApi { final ApiClient apiClient; + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future createPartnerDeprecatedWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/partners/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Parameters: + /// + /// * [String] id (required): + Future createPartnerDeprecated(String id,) async { + final response = await createPartnerDeprecatedWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PartnerResponseDto',) as PartnerResponseDto; + + } + return null; + } + /// This property was deprecated in v1.116.0. This endpoint requires the `asset.read` permission. /// /// Note: This method returns the HTTP [Response]. diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index eb5d5f5806..a5fdf53ab5 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -22,8 +22,60 @@ class PartnersApi { /// /// Parameters: /// + /// * [PartnerCreateDto] partnerCreateDto (required): + Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/partners'; + + // ignore: prefer_final_locals + Object? postBody = partnerCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This endpoint requires the `partner.create` permission. + /// + /// Parameters: + /// + /// * [PartnerCreateDto] partnerCreateDto (required): + Future createPartner(PartnerCreateDto partnerCreateDto,) async { + final response = await createPartnerWithHttpInfo(partnerCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PartnerResponseDto',) as PartnerResponseDto; + + } + return null; + } + + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// /// * [String] id (required): - Future createPartnerWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id,) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -49,13 +101,13 @@ class PartnersApi { ); } - /// This endpoint requires the `partner.create` permission. + /// This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. /// /// Parameters: /// /// * [String] id (required): - Future createPartner(String id,) async { - final response = await createPartnerWithHttpInfo(id,); + Future createPartnerDeprecated(String id,) async { + final response = await createPartnerDeprecatedWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -179,14 +231,14 @@ class PartnersApi { /// /// * [String] id (required): /// - /// * [UpdatePartnerDto] updatePartnerDto (required): - Future updatePartnerWithHttpInfo(String id, UpdatePartnerDto updatePartnerDto,) async { + /// * [PartnerUpdateDto] partnerUpdateDto (required): + Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = updatePartnerDto; + Object? postBody = partnerUpdateDto; final queryParams = []; final headerParams = {}; @@ -212,9 +264,9 @@ class PartnersApi { /// /// * [String] id (required): /// - /// * [UpdatePartnerDto] updatePartnerDto (required): - Future updatePartner(String id, UpdatePartnerDto updatePartnerDto,) async { - final response = await updatePartnerWithHttpInfo(id, updatePartnerDto,); + /// * [PartnerUpdateDto] partnerUpdateDto (required): + Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto,) async { + final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 3ea3b3c3e3..ae5fd9227b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -434,10 +434,14 @@ class ApiClient { return OnboardingDto.fromJson(value); case 'OnboardingResponseDto': return OnboardingResponseDto.fromJson(value); + case 'PartnerCreateDto': + return PartnerCreateDto.fromJson(value); case 'PartnerDirection': return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': return PartnerResponseDto.fromJson(value); + case 'PartnerUpdateDto': + return PartnerUpdateDto.fromJson(value); case 'PeopleResponse': return PeopleResponse.fromJson(value); case 'PeopleResponseDto': @@ -734,8 +738,6 @@ class ApiClient { return UpdateAssetDto.fromJson(value); case 'UpdateLibraryDto': return UpdateLibraryDto.fromJson(value); - case 'UpdatePartnerDto': - return UpdatePartnerDto.fromJson(value); case 'UsageByUserDto': return UsageByUserDto.fromJson(value); case 'UserAdminCreateDto': diff --git a/mobile/openapi/lib/model/partner_create_dto.dart b/mobile/openapi/lib/model/partner_create_dto.dart new file mode 100644 index 0000000000..09d60c5c77 --- /dev/null +++ b/mobile/openapi/lib/model/partner_create_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PartnerCreateDto { + /// Returns a new [PartnerCreateDto] instance. + PartnerCreateDto({ + required this.sharedWithId, + }); + + String sharedWithId; + + @override + bool operator ==(Object other) => identical(this, other) || other is PartnerCreateDto && + other.sharedWithId == sharedWithId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (sharedWithId.hashCode); + + @override + String toString() => 'PartnerCreateDto[sharedWithId=$sharedWithId]'; + + Map toJson() { + final json = {}; + json[r'sharedWithId'] = this.sharedWithId; + return json; + } + + /// Returns a new [PartnerCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PartnerCreateDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerCreateDto"); + if (value is Map) { + final json = value.cast(); + + return PartnerCreateDto( + sharedWithId: mapValueOfType(json, r'sharedWithId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PartnerCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PartnerCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PartnerCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PartnerCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'sharedWithId', + }; +} + diff --git a/mobile/openapi/lib/model/update_partner_dto.dart b/mobile/openapi/lib/model/partner_update_dto.dart similarity index 66% rename from mobile/openapi/lib/model/update_partner_dto.dart rename to mobile/openapi/lib/model/partner_update_dto.dart index 3af3c83ad1..25cf217764 100644 --- a/mobile/openapi/lib/model/update_partner_dto.dart +++ b/mobile/openapi/lib/model/partner_update_dto.dart @@ -10,16 +10,16 @@ part of openapi.api; -class UpdatePartnerDto { - /// Returns a new [UpdatePartnerDto] instance. - UpdatePartnerDto({ +class PartnerUpdateDto { + /// Returns a new [PartnerUpdateDto] instance. + PartnerUpdateDto({ required this.inTimeline, }); bool inTimeline; @override - bool operator ==(Object other) => identical(this, other) || other is UpdatePartnerDto && + bool operator ==(Object other) => identical(this, other) || other is PartnerUpdateDto && other.inTimeline == inTimeline; @override @@ -28,7 +28,7 @@ class UpdatePartnerDto { (inTimeline.hashCode); @override - String toString() => 'UpdatePartnerDto[inTimeline=$inTimeline]'; + String toString() => 'PartnerUpdateDto[inTimeline=$inTimeline]'; Map toJson() { final json = {}; @@ -36,26 +36,26 @@ class UpdatePartnerDto { return json; } - /// Returns a new [UpdatePartnerDto] instance and imports its values from + /// Returns a new [PartnerUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static UpdatePartnerDto? fromJson(dynamic value) { - upgradeDto(value, "UpdatePartnerDto"); + static PartnerUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerUpdateDto"); if (value is Map) { final json = value.cast(); - return UpdatePartnerDto( + return PartnerUpdateDto( inTimeline: mapValueOfType(json, r'inTimeline')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = UpdatePartnerDto.fromJson(row); + final value = PartnerUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -64,12 +64,12 @@ class UpdatePartnerDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = UpdatePartnerDto.fromJson(entry.value); + final value = PartnerUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -78,14 +78,14 @@ class UpdatePartnerDto { return map; } - // maps a json object with a list of UpdatePartnerDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of PartnerUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = UpdatePartnerDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = PartnerUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e148e7efae..5a7886253e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4994,6 +4994,48 @@ ], "x-immich-permission": "partner.read", "description": "This endpoint requires the `partner.read` permission." + }, + "post": { + "operationId": "createPartner", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Partners" + ], + "x-immich-permission": "partner.create", + "description": "This endpoint requires the `partner.create` permission." } }, "/partners/{id}": { @@ -5033,7 +5075,9 @@ "description": "This endpoint requires the `partner.delete` permission." }, "post": { - "operationId": "createPartner", + "deprecated": true, + "description": "This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission.", + "operationId": "createPartnerDeprecated", "parameters": [ { "name": "id", @@ -5069,10 +5113,13 @@ } ], "tags": [ - "Partners" + "Partners", + "Deprecated" ], - "x-immich-permission": "partner.create", - "description": "This endpoint requires the `partner.create` permission." + "x-immich-lifecycle": { + "deprecatedAt": "v1.141.0" + }, + "x-immich-permission": "partner.create" }, "put": { "operationId": "updatePartner", @@ -5091,7 +5138,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdatePartnerDto" + "$ref": "#/components/schemas/PartnerUpdateDto" } } }, @@ -12853,6 +12900,18 @@ ], "type": "object" }, + "PartnerCreateDto": { + "properties": { + "sharedWithId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "sharedWithId" + ], + "type": "object" + }, "PartnerDirection": { "enum": [ "shared-by", @@ -12899,6 +12958,17 @@ ], "type": "object" }, + "PartnerUpdateDto": { + "properties": { + "inTimeline": { + "type": "boolean" + } + }, + "required": [ + "inTimeline" + ], + "type": "object" + }, "PeopleResponse": { "properties": { "enabled": { @@ -17241,17 +17311,6 @@ }, "type": "object" }, - "UpdatePartnerDto": { - "properties": { - "inTimeline": { - "type": "boolean" - } - }, - "required": [ - "inTimeline" - ], - "type": "object" - }, "UsageByUserDto": { "properties": { "photos": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c642dc93a4..a74afd733d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -811,7 +811,10 @@ export type PartnerResponseDto = { profileChangedAt: string; profileImagePath: string; }; -export type UpdatePartnerDto = { +export type PartnerCreateDto = { + sharedWithId: string; +}; +export type PartnerUpdateDto = { inTimeline: boolean; }; export type PeopleResponseDto = { @@ -3122,6 +3125,21 @@ export function getPartners({ direction }: { ...opts })); } +/** + * This endpoint requires the `partner.create` permission. + */ +export function createPartner({ partnerCreateDto }: { + partnerCreateDto: PartnerCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: PartnerResponseDto; + }>("/partners", oazapfts.json({ + ...opts, + method: "POST", + body: partnerCreateDto + }))); +} /** * This endpoint requires the `partner.delete` permission. */ @@ -3134,9 +3152,9 @@ export function removePartner({ id }: { })); } /** - * This endpoint requires the `partner.create` permission. + * This property was deprecated in v1.141.0. This endpoint requires the `partner.create` permission. */ -export function createPartner({ id }: { +export function createPartnerDeprecated({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ @@ -3150,9 +3168,9 @@ export function createPartner({ id }: { /** * This endpoint requires the `partner.update` permission. */ -export function updatePartner({ id, updatePartnerDto }: { +export function updatePartner({ id, partnerUpdateDto }: { id: string; - updatePartnerDto: UpdatePartnerDto; + partnerUpdateDto: PartnerUpdateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3160,7 +3178,7 @@ export function updatePartner({ id, updatePartnerDto }: { }>(`/partners/${encodeURIComponent(id)}`, oazapfts.json({ ...opts, method: "PUT", - body: updatePartnerDto + body: partnerUpdateDto }))); } /** diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts new file mode 100644 index 0000000000..2c507a634f --- /dev/null +++ b/server/src/controllers/partner.controller.spec.ts @@ -0,0 +1,101 @@ +import { PartnerController } from 'src/controllers/partner.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PartnerService } from 'src/services/partner.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(PartnerController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(PartnerService); + + beforeAll(async () => { + ctx = await controllerSetup(PartnerController, [ + { provide: PartnerService, useValue: service }, + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /partners', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/partners'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require a direction`, async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'direction should not be empty', + expect.stringContaining('direction must be one of the following values:'), + ]), + ); + }); + + it(`should require direction to be an enum`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/partners`) + .query({ direction: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([expect.stringContaining('direction must be one of the following values:')]), + ); + }); + }); + + describe('POST /partners', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/partners'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require sharedWithId to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post(`/partners`) + .send({ sharedWithId: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); + + describe('PUT /partners/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/partners/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require id to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/partners/invalid`) + .send({ inTimeline: true }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); + + describe('DELETE /partners/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/partners/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require id to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/partners/invalid`) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); +}); diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index f2f4e3d7d6..7cb5c1c274 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -1,7 +1,8 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { EndpointLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PartnerService } from 'src/services/partner.service'; @@ -18,10 +19,17 @@ export class PartnerController { return this.service.search(auth, dto); } - @Post(':id') + @Post() @Authenticated({ permission: Permission.PartnerCreate }) - createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.create(auth, id); + createPartner(@Auth() auth: AuthDto, @Body() dto: PartnerCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Post(':id') + @EndpointLifecycle({ deprecatedAt: 'v1.141.0' }) + @Authenticated({ permission: Permission.PartnerCreate }) + createPartnerDeprecated(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.create(auth, { sharedWithId: id }); } @Put(':id') @@ -29,7 +37,7 @@ export class PartnerController { updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: UpdatePartnerDto, + @Body() dto: PartnerUpdateDto, ): Promise { return this.service.update(auth, id, dto); } diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 28d4adf8b7..599213f662 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,9 +1,14 @@ import { IsNotEmpty } from 'class-validator'; import { UserResponseDto } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; -import { ValidateEnum } from 'src/validation'; +import { ValidateEnum, ValidateUUID } from 'src/validation'; -export class UpdatePartnerDto { +export class PartnerCreateDto { + @ValidateUUID() + sharedWithId!: string; +} + +export class PartnerUpdateDto { @IsNotEmpty() inTimeline!: boolean; } diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index c6d5762c2c..db057a453a 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -53,7 +53,7 @@ describe(PartnerService.name, () => { mocks.partner.get.mockResolvedValue(void 0); mocks.partner.create.mockResolvedValue(partner); - await expect(sut.create(auth, user2.id)).resolves.toBeDefined(); + await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined(); expect(mocks.partner.create).toHaveBeenCalledWith({ sharedById: partner.sharedById, @@ -69,7 +69,7 @@ describe(PartnerService.name, () => { mocks.partner.get.mockResolvedValue(partner); - await expect(sut.create(auth, user2.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.partner.create).not.toHaveBeenCalled(); }); diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index 755b688397..628efa9d49 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Partner } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; -import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; import { Permission } from 'src/enum'; import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository'; @@ -9,7 +9,7 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class PartnerService extends BaseService { - async create(auth: AuthDto, sharedWithId: string): Promise { + async create(auth: AuthDto, { sharedWithId }: PartnerCreateDto): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const exists = await this.partnerRepository.get(partnerId); if (exists) { @@ -39,7 +39,7 @@ export class PartnerService extends BaseService { .map((partner) => this.mapPartner(partner, direction)); } - async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { + async update(auth: AuthDto, sharedById: string, dto: PartnerUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.PartnerUpdate, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; 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 586cdf4afa..e847ea2018 100644 --- a/web/src/lib/components/user-settings-page/partner-settings.svelte +++ b/web/src/lib/components/user-settings-page/partner-settings.svelte @@ -104,7 +104,7 @@ try { for (const user of users) { - await createPartner({ id: user.id }); + await createPartner({ partnerCreateDto: { sharedWithId: user.id } }); } await refreshPartners(); @@ -115,7 +115,7 @@ const handleShowOnTimelineChanged = async (partner: PartnerSharing, inTimeline: boolean) => { try { - await updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } }); + await updatePartner({ id: partner.user.id, partnerUpdateDto: { inTimeline } }); partner.inTimeline = inTimeline; } catch (error) {