Merge branch 'main' into chore/backup-wakelock

This commit is contained in:
Saschl 2024-08-20 15:20:56 +02:00 committed by GitHub
commit e69eab18b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1195 additions and 1790 deletions

View File

@ -344,16 +344,16 @@ describe('/albums', () => {
}); });
}); });
describe('GET /albums/count', () => { describe('GET /albums/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/count'); const { status, body } = await request(app).get('/albums/statistics');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return total count of albums the user has access to', async () => { it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums/count') .get('/albums/statistics')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);

View File

@ -363,6 +363,8 @@ describe('/asset', () => {
utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken),
]); ]);
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
}); });
it('should require authentication', async () => { it('should require authentication', async () => {

View File

@ -18,7 +18,7 @@ class ApiService implements Authentication {
late AlbumsApi albumsApi; late AlbumsApi albumsApi;
late AssetsApi assetsApi; late AssetsApi assetsApi;
late SearchApi searchApi; late SearchApi searchApi;
late ServerInfoApi serverInfoApi; late ServerApi serverInfoApi;
late MapApi mapApi; late MapApi mapApi;
late PartnersApi partnersApi; late PartnersApi partnersApi;
late PeopleApi peopleApi; late PeopleApi peopleApi;
@ -50,7 +50,7 @@ class ApiService implements Authentication {
oAuthApi = OAuthApi(_apiClient); oAuthApi = OAuthApi(_apiClient);
albumsApi = AlbumsApi(_apiClient); albumsApi = AlbumsApi(_apiClient);
assetsApi = AssetsApi(_apiClient); assetsApi = AssetsApi(_apiClient);
serverInfoApi = ServerInfoApi(_apiClient); serverInfoApi = ServerApi(_apiClient);
searchApi = SearchApi(_apiClient); searchApi = SearchApi(_apiClient);
mapApi = MapApi(_apiClient); mapApi = MapApi(_apiClient);
partnersApi = PartnersApi(_apiClient); partnersApi = PartnersApi(_apiClient);

View File

@ -86,8 +86,8 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users | *AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users |
*AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums |
*AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} |
*AlbumsApi* | [**getAlbumCount**](doc//AlbumsApi.md#getalbumcount) | **GET** /albums/count |
*AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} |
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics |
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums |
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets |
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
@ -115,16 +115,7 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeprecatedApi* | [**getAboutInfo**](doc//DeprecatedApi.md#getaboutinfo) | **GET** /server-info/about |
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | *DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
*DeprecatedApi* | [**getServerConfig**](doc//DeprecatedApi.md#getserverconfig) | **GET** /server-info/config |
*DeprecatedApi* | [**getServerFeatures**](doc//DeprecatedApi.md#getserverfeatures) | **GET** /server-info/features |
*DeprecatedApi* | [**getServerStatistics**](doc//DeprecatedApi.md#getserverstatistics) | **GET** /server-info/statistics |
*DeprecatedApi* | [**getServerVersion**](doc//DeprecatedApi.md#getserverversion) | **GET** /server-info/version |
*DeprecatedApi* | [**getStorage**](doc//DeprecatedApi.md#getstorage) | **GET** /server-info/storage |
*DeprecatedApi* | [**getSupportedMediaTypes**](doc//DeprecatedApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
*DeprecatedApi* | [**getTheme**](doc//DeprecatedApi.md#gettheme) | **GET** /server-info/theme |
*DeprecatedApi* | [**pingServer**](doc//DeprecatedApi.md#pingserver) | **GET** /server-info/ping |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
@ -182,17 +173,17 @@ Class | Method | HTTP request | Description
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
*ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config |
*ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features |
*ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license |
*ServerApi* | [**getServerStatistics**](doc//ServerApi.md#getserverstatistics) | **GET** /server/statistics |
*ServerApi* | [**getServerVersion**](doc//ServerApi.md#getserverversion) | **GET** /server/version |
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
*ServerInfoApi* | [**getAboutInfo**](doc//ServerInfoApi.md#getaboutinfo) | **GET** /server-info/about |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerStatistics**](doc//ServerInfoApi.md#getserverstatistics) | **GET** /server-info/statistics |
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
*ServerInfoApi* | [**getStorage**](doc//ServerInfoApi.md#getstorage) | **GET** /server-info/storage |
*ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
@ -265,8 +256,8 @@ Class | Method | HTTP request | Description
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md) - [AddUsersDto](doc//AddUsersDto.md)
- [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md)
- [AlbumUserAddDto](doc//AlbumUserAddDto.md) - [AlbumUserAddDto](doc//AlbumUserAddDto.md)
- [AlbumUserCreateDto](doc//AlbumUserCreateDto.md) - [AlbumUserCreateDto](doc//AlbumUserCreateDto.md)
- [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md)

View File

@ -51,7 +51,6 @@ part 'api/partners_api.dart';
part 'api/people_api.dart'; part 'api/people_api.dart';
part 'api/search_api.dart'; part 'api/search_api.dart';
part 'api/server_api.dart'; part 'api/server_api.dart';
part 'api/server_info_api.dart';
part 'api/sessions_api.dart'; part 'api/sessions_api.dart';
part 'api/shared_links_api.dart'; part 'api/shared_links_api.dart';
part 'api/stacks_api.dart'; part 'api/stacks_api.dart';
@ -73,8 +72,8 @@ part 'model/activity_response_dto.dart';
part 'model/activity_statistics_response_dto.dart'; part 'model/activity_statistics_response_dto.dart';
part 'model/add_users_dto.dart'; part 'model/add_users_dto.dart';
part 'model/admin_onboarding_update_dto.dart'; part 'model/admin_onboarding_update_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart'; part 'model/album_response_dto.dart';
part 'model/album_statistics_response_dto.dart';
part 'model/album_user_add_dto.dart'; part 'model/album_user_add_dto.dart';
part 'model/album_user_create_dto.dart'; part 'model/album_user_create_dto.dart';
part 'model/album_user_response_dto.dart'; part 'model/album_user_response_dto.dart';

View File

@ -218,47 +218,6 @@ class AlbumsApi {
} }
} }
/// Performs an HTTP 'GET /albums/count' operation and returns the [Response].
Future<Response> getAlbumCountWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/albums/count';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<AlbumCountResponseDto?> getAlbumCount() async {
final response = await getAlbumCountWithHttpInfo();
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), 'AlbumCountResponseDto',) as AlbumCountResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /albums/{id}' operation and returns the [Response]. /// Performs an HTTP 'GET /albums/{id}' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
@ -322,6 +281,47 @@ class AlbumsApi {
return null; return null;
} }
/// Performs an HTTP 'GET /albums/statistics' operation and returns the [Response].
Future<Response> getAlbumStatisticsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/albums/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<AlbumStatisticsResponseDto?> getAlbumStatistics() async {
final response = await getAlbumStatisticsWithHttpInfo();
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), 'AlbumStatisticsResponseDto',) as AlbumStatisticsResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /albums' operation and returns the [Response]. /// Performs an HTTP 'GET /albums' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@ -16,50 +16,6 @@ class DeprecatedApi {
final ApiClient apiClient; final ApiClient apiClient;
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getAboutInfoWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/about';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerAboutResponseDto?> getAboutInfo() async {
final response = await getAboutInfoWithHttpInfo();
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), 'ServerAboutResponseDto',) as ServerAboutResponseDto;
}
return null;
}
/// This property was deprecated in v1.113.0 /// This property was deprecated in v1.113.0
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
@ -115,356 +71,4 @@ class DeprecatedApi {
} }
return null; return null;
} }
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/config';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerConfigDto?> getServerConfig() async {
final response = await getServerConfigWithHttpInfo();
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), 'ServerConfigDto',) as ServerConfigDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerFeaturesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/features';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerFeaturesDto?> getServerFeatures() async {
final response = await getServerFeaturesWithHttpInfo();
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), 'ServerFeaturesDto',) as ServerFeaturesDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerStatisticsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerStatsResponseDto?> getServerStatistics() async {
final response = await getServerStatisticsWithHttpInfo();
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), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerVersionWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/version';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerVersionResponseDto?> getServerVersion() async {
final response = await getServerVersionWithHttpInfo();
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), 'ServerVersionResponseDto',) as ServerVersionResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getStorageWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/storage';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerStorageResponseDto?> getStorage() async {
final response = await getStorageWithHttpInfo();
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), 'ServerStorageResponseDto',) as ServerStorageResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getSupportedMediaTypesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/media-types';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerMediaTypesResponseDto?> getSupportedMediaTypes() async {
final response = await getSupportedMediaTypesWithHttpInfo();
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), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getThemeWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/theme';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerThemeDto?> getTheme() async {
final response = await getThemeWithHttpInfo();
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), 'ServerThemeDto',) as ServerThemeDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/ping';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerPingResponse?> pingServer() async {
final response = await pingServerWithHttpInfo();
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), 'ServerPingResponse',) as ServerPingResponse;
}
return null;
}
} }

View File

@ -49,6 +49,129 @@ class ServerApi {
} }
} }
/// Performs an HTTP 'GET /server/about' operation and returns the [Response].
Future<Response> getAboutInfoWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/about';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerAboutResponseDto?> getAboutInfo() async {
final response = await getAboutInfoWithHttpInfo();
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), 'ServerAboutResponseDto',) as ServerAboutResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /server/config' operation and returns the [Response].
Future<Response> getServerConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/config';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerConfigDto?> getServerConfig() async {
final response = await getServerConfigWithHttpInfo();
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), 'ServerConfigDto',) as ServerConfigDto;
}
return null;
}
/// Performs an HTTP 'GET /server/features' operation and returns the [Response].
Future<Response> getServerFeaturesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/features';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerFeaturesDto?> getServerFeatures() async {
final response = await getServerFeaturesWithHttpInfo();
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), 'ServerFeaturesDto',) as ServerFeaturesDto;
}
return null;
}
/// Performs an HTTP 'GET /server/license' operation and returns the [Response]. /// Performs an HTTP 'GET /server/license' operation and returns the [Response].
Future<Response> getServerLicenseWithHttpInfo() async { Future<Response> getServerLicenseWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
@ -90,6 +213,252 @@ class ServerApi {
return null; return null;
} }
/// Performs an HTTP 'GET /server/statistics' operation and returns the [Response].
Future<Response> getServerStatisticsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerStatsResponseDto?> getServerStatistics() async {
final response = await getServerStatisticsWithHttpInfo();
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), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /server/version' operation and returns the [Response].
Future<Response> getServerVersionWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/version';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerVersionResponseDto?> getServerVersion() async {
final response = await getServerVersionWithHttpInfo();
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), 'ServerVersionResponseDto',) as ServerVersionResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /server/storage' operation and returns the [Response].
Future<Response> getStorageWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/storage';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerStorageResponseDto?> getStorage() async {
final response = await getStorageWithHttpInfo();
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), 'ServerStorageResponseDto',) as ServerStorageResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /server/media-types' operation and returns the [Response].
Future<Response> getSupportedMediaTypesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/media-types';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerMediaTypesResponseDto?> getSupportedMediaTypes() async {
final response = await getSupportedMediaTypesWithHttpInfo();
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), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /server/theme' operation and returns the [Response].
Future<Response> getThemeWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/theme';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerThemeDto?> getTheme() async {
final response = await getThemeWithHttpInfo();
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), 'ServerThemeDto',) as ServerThemeDto;
}
return null;
}
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/ping';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerPingResponse?> pingServer() async {
final response = await pingServerWithHttpInfo();
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), 'ServerPingResponse',) as ServerPingResponse;
}
return null;
}
/// Performs an HTTP 'PUT /server/license' operation and returns the [Response]. /// Performs an HTTP 'PUT /server/license' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@ -1,414 +0,0 @@
//
// 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 ServerInfoApi {
ServerInfoApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getAboutInfoWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/about';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerAboutResponseDto?> getAboutInfo() async {
final response = await getAboutInfoWithHttpInfo();
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), 'ServerAboutResponseDto',) as ServerAboutResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/config';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerConfigDto?> getServerConfig() async {
final response = await getServerConfigWithHttpInfo();
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), 'ServerConfigDto',) as ServerConfigDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerFeaturesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/features';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerFeaturesDto?> getServerFeatures() async {
final response = await getServerFeaturesWithHttpInfo();
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), 'ServerFeaturesDto',) as ServerFeaturesDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerStatisticsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerStatsResponseDto?> getServerStatistics() async {
final response = await getServerStatisticsWithHttpInfo();
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), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getServerVersionWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/version';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerVersionResponseDto?> getServerVersion() async {
final response = await getServerVersionWithHttpInfo();
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), 'ServerVersionResponseDto',) as ServerVersionResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getStorageWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/storage';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerStorageResponseDto?> getStorage() async {
final response = await getStorageWithHttpInfo();
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), 'ServerStorageResponseDto',) as ServerStorageResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getSupportedMediaTypesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/media-types';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerMediaTypesResponseDto?> getSupportedMediaTypes() async {
final response = await getSupportedMediaTypesWithHttpInfo();
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), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> getThemeWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/theme';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerThemeDto?> getTheme() async {
final response = await getThemeWithHttpInfo();
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), 'ServerThemeDto',) as ServerThemeDto;
}
return null;
}
/// This property was deprecated in v1.107.0
///
/// Note: This method returns the HTTP [Response].
Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/ping';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.107.0
Future<ServerPingResponse?> pingServer() async {
final response = await pingServerWithHttpInfo();
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), 'ServerPingResponse',) as ServerPingResponse;
}
return null;
}
}

View File

@ -201,10 +201,10 @@ class ApiClient {
return AddUsersDto.fromJson(value); return AddUsersDto.fromJson(value);
case 'AdminOnboardingUpdateDto': case 'AdminOnboardingUpdateDto':
return AdminOnboardingUpdateDto.fromJson(value); return AdminOnboardingUpdateDto.fromJson(value);
case 'AlbumCountResponseDto':
return AlbumCountResponseDto.fromJson(value);
case 'AlbumResponseDto': case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value); return AlbumResponseDto.fromJson(value);
case 'AlbumStatisticsResponseDto':
return AlbumStatisticsResponseDto.fromJson(value);
case 'AlbumUserAddDto': case 'AlbumUserAddDto':
return AlbumUserAddDto.fromJson(value); return AlbumUserAddDto.fromJson(value);
case 'AlbumUserCreateDto': case 'AlbumUserCreateDto':

View File

@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class AlbumCountResponseDto { class AlbumStatisticsResponseDto {
/// Returns a new [AlbumCountResponseDto] instance. /// Returns a new [AlbumStatisticsResponseDto] instance.
AlbumCountResponseDto({ AlbumStatisticsResponseDto({
required this.notShared, required this.notShared,
required this.owned, required this.owned,
required this.shared, required this.shared,
@ -25,7 +25,7 @@ class AlbumCountResponseDto {
int shared; int shared;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumStatisticsResponseDto &&
other.notShared == notShared && other.notShared == notShared &&
other.owned == owned && other.owned == owned &&
other.shared == shared; other.shared == shared;
@ -38,7 +38,7 @@ class AlbumCountResponseDto {
(shared.hashCode); (shared.hashCode);
@override @override
String toString() => 'AlbumCountResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; String toString() => 'AlbumStatisticsResponseDto[notShared=$notShared, owned=$owned, shared=$shared]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -48,14 +48,14 @@ class AlbumCountResponseDto {
return json; return json;
} }
/// Returns a new [AlbumCountResponseDto] instance and imports its values from /// Returns a new [AlbumStatisticsResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static AlbumCountResponseDto? fromJson(dynamic value) { static AlbumStatisticsResponseDto? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AlbumCountResponseDto( return AlbumStatisticsResponseDto(
notShared: mapValueOfType<int>(json, r'notShared')!, notShared: mapValueOfType<int>(json, r'notShared')!,
owned: mapValueOfType<int>(json, r'owned')!, owned: mapValueOfType<int>(json, r'owned')!,
shared: mapValueOfType<int>(json, r'shared')!, shared: mapValueOfType<int>(json, r'shared')!,
@ -64,11 +64,11 @@ class AlbumCountResponseDto {
return null; return null;
} }
static List<AlbumCountResponseDto> listFromJson(dynamic json, {bool growable = false,}) { static List<AlbumStatisticsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumCountResponseDto>[]; final result = <AlbumStatisticsResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = AlbumCountResponseDto.fromJson(row); final value = AlbumStatisticsResponseDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@ -77,12 +77,12 @@ class AlbumCountResponseDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, AlbumCountResponseDto> mapFromJson(dynamic json) { static Map<String, AlbumStatisticsResponseDto> mapFromJson(dynamic json) {
final map = <String, AlbumCountResponseDto>{}; final map = <String, AlbumStatisticsResponseDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AlbumCountResponseDto.fromJson(entry.value); final value = AlbumStatisticsResponseDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@ -91,14 +91,14 @@ class AlbumCountResponseDto {
return map; return map;
} }
// maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map // maps a json object with a list of AlbumStatisticsResponseDto-objects as value to a dart map
static Map<String, List<AlbumCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<AlbumStatisticsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumCountResponseDto>>{}; final map = <String, List<AlbumStatisticsResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = AlbumCountResponseDto.listFromJson(entry.value, growable: growable,); map[entry.key] = AlbumStatisticsResponseDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;

View File

@ -36,7 +36,6 @@ class Permission {
static const assetPeriodRead = Permission._(r'asset.read'); static const assetPeriodRead = Permission._(r'asset.read');
static const assetPeriodUpdate = Permission._(r'asset.update'); static const assetPeriodUpdate = Permission._(r'asset.update');
static const assetPeriodDelete = Permission._(r'asset.delete'); static const assetPeriodDelete = Permission._(r'asset.delete');
static const assetPeriodRestore = Permission._(r'asset.restore');
static const assetPeriodShare = Permission._(r'asset.share'); static const assetPeriodShare = Permission._(r'asset.share');
static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodView = Permission._(r'asset.view');
static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodDownload = Permission._(r'asset.download');
@ -78,6 +77,9 @@ class Permission {
static const personPeriodStatistics = Permission._(r'person.statistics'); static const personPeriodStatistics = Permission._(r'person.statistics');
static const personPeriodMerge = Permission._(r'person.merge'); static const personPeriodMerge = Permission._(r'person.merge');
static const personPeriodReassign = Permission._(r'person.reassign'); static const personPeriodReassign = Permission._(r'person.reassign');
static const sessionPeriodRead = Permission._(r'session.read');
static const sessionPeriodUpdate = Permission._(r'session.update');
static const sessionPeriodDelete = Permission._(r'session.delete');
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create'); static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
@ -114,7 +116,6 @@ class Permission {
assetPeriodRead, assetPeriodRead,
assetPeriodUpdate, assetPeriodUpdate,
assetPeriodDelete, assetPeriodDelete,
assetPeriodRestore,
assetPeriodShare, assetPeriodShare,
assetPeriodView, assetPeriodView,
assetPeriodDownload, assetPeriodDownload,
@ -156,6 +157,9 @@ class Permission {
personPeriodStatistics, personPeriodStatistics,
personPeriodMerge, personPeriodMerge,
personPeriodReassign, personPeriodReassign,
sessionPeriodRead,
sessionPeriodUpdate,
sessionPeriodDelete,
sharedLinkPeriodCreate, sharedLinkPeriodCreate,
sharedLinkPeriodRead, sharedLinkPeriodRead,
sharedLinkPeriodUpdate, sharedLinkPeriodUpdate,
@ -227,7 +231,6 @@ class PermissionTypeTransformer {
case r'asset.read': return Permission.assetPeriodRead; case r'asset.read': return Permission.assetPeriodRead;
case r'asset.update': return Permission.assetPeriodUpdate; case r'asset.update': return Permission.assetPeriodUpdate;
case r'asset.delete': return Permission.assetPeriodDelete; case r'asset.delete': return Permission.assetPeriodDelete;
case r'asset.restore': return Permission.assetPeriodRestore;
case r'asset.share': return Permission.assetPeriodShare; case r'asset.share': return Permission.assetPeriodShare;
case r'asset.view': return Permission.assetPeriodView; case r'asset.view': return Permission.assetPeriodView;
case r'asset.download': return Permission.assetPeriodDownload; case r'asset.download': return Permission.assetPeriodDownload;
@ -269,6 +272,9 @@ class PermissionTypeTransformer {
case r'person.statistics': return Permission.personPeriodStatistics; case r'person.statistics': return Permission.personPeriodStatistics;
case r'person.merge': return Permission.personPeriodMerge; case r'person.merge': return Permission.personPeriodMerge;
case r'person.reassign': return Permission.personPeriodReassign; case r'person.reassign': return Permission.personPeriodReassign;
case r'session.read': return Permission.sessionPeriodRead;
case r'session.update': return Permission.sessionPeriodUpdate;
case r'session.delete': return Permission.sessionPeriodDelete;
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate; case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
case r'sharedLink.read': return Permission.sharedLinkPeriodRead; case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;

View File

@ -660,16 +660,16 @@
] ]
} }
}, },
"/albums/count": { "/albums/statistics": {
"get": { "get": {
"operationId": "getAlbumCount", "operationId": "getAlbumStatistics",
"parameters": [], "parameters": [],
"responses": { "responses": {
"200": { "200": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/AlbumCountResponseDto" "$ref": "#/components/schemas/AlbumStatisticsResponseDto"
} }
} }
}, },
@ -3643,7 +3643,7 @@
"operationId": "unlinkOAuthAccount", "operationId": "unlinkOAuthAccount",
"parameters": [], "parameters": [],
"responses": { "responses": {
"201": { "200": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -4771,10 +4771,8 @@
] ]
} }
}, },
"/server-info/about": { "/server/about": {
"get": { "get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getAboutInfo", "operationId": "getAboutInfo",
"parameters": [], "parameters": [],
"responses": { "responses": {
@ -4801,18 +4799,12 @@
} }
], ],
"tags": [ "tags": [
"Server Info", "Server"
"Deprecated" ]
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
} }
}, },
"/server-info/config": { "/server/config": {
"get": { "get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getServerConfig", "operationId": "getServerConfig",
"parameters": [], "parameters": [],
"responses": { "responses": {
@ -4828,18 +4820,12 @@
} }
}, },
"tags": [ "tags": [
"Server Info", "Server"
"Deprecated" ]
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
} }
}, },
"/server-info/features": { "/server/features": {
"get": { "get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getServerFeatures", "operationId": "getServerFeatures",
"parameters": [], "parameters": [],
"responses": { "responses": {
@ -4855,196 +4841,8 @@
} }
}, },
"tags": [ "tags": [
"Server Info", "Server"
"Deprecated" ]
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/media-types": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getSupportedMediaTypes",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerMediaTypesResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/ping": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "pingServer",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerPingResponse"
}
}
},
"description": ""
}
},
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/statistics": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getServerStatistics",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerStatsResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/storage": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getStorage",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerStorageResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/theme": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getTheme",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerThemeDto"
}
}
},
"description": ""
}
},
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
}
},
"/server-info/version": {
"get": {
"deprecated": true,
"description": "This property was deprecated in v1.107.0",
"operationId": "getServerVersion",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerVersionResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"Server Info",
"Deprecated"
],
"x-immich-lifecycle": {
"deprecatedAt": "v1.107.0"
}
} }
}, },
"/server/license": { "/server/license": {
@ -5145,6 +4943,154 @@
] ]
} }
}, },
"/server/media-types": {
"get": {
"operationId": "getSupportedMediaTypes",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerMediaTypesResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"Server"
]
}
},
"/server/ping": {
"get": {
"operationId": "pingServer",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerPingResponse"
}
}
},
"description": ""
}
},
"tags": [
"Server"
]
}
},
"/server/statistics": {
"get": {
"operationId": "getServerStatistics",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerStatsResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Server"
]
}
},
"/server/storage": {
"get": {
"operationId": "getStorage",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerStorageResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Server"
]
}
},
"/server/theme": {
"get": {
"operationId": "getTheme",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerThemeDto"
}
}
},
"description": ""
}
},
"tags": [
"Server"
]
}
},
"/server/version": {
"get": {
"operationId": "getServerVersion",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerVersionResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"Server"
]
}
},
"/sessions": { "/sessions": {
"delete": { "delete": {
"operationId": "deleteAllSessions", "operationId": "deleteAllSessions",
@ -7505,25 +7451,6 @@
], ],
"type": "object" "type": "object"
}, },
"AlbumCountResponseDto": {
"properties": {
"notShared": {
"type": "integer"
},
"owned": {
"type": "integer"
},
"shared": {
"type": "integer"
}
},
"required": [
"notShared",
"owned",
"shared"
],
"type": "object"
},
"AlbumResponseDto": { "AlbumResponseDto": {
"properties": { "properties": {
"albumName": { "albumName": {
@ -7611,6 +7538,25 @@
], ],
"type": "object" "type": "object"
}, },
"AlbumStatisticsResponseDto": {
"properties": {
"notShared": {
"type": "integer"
},
"owned": {
"type": "integer"
},
"shared": {
"type": "integer"
}
},
"required": [
"notShared",
"owned",
"shared"
],
"type": "object"
},
"AlbumUserAddDto": { "AlbumUserAddDto": {
"properties": { "properties": {
"role": { "role": {
@ -9972,7 +9918,6 @@
"asset.read", "asset.read",
"asset.update", "asset.update",
"asset.delete", "asset.delete",
"asset.restore",
"asset.share", "asset.share",
"asset.view", "asset.view",
"asset.download", "asset.download",
@ -10014,6 +9959,9 @@
"person.statistics", "person.statistics",
"person.merge", "person.merge",
"person.reassign", "person.reassign",
"session.read",
"session.update",
"session.delete",
"sharedLink.create", "sharedLink.create",
"sharedLink.read", "sharedLink.read",
"sharedLink.update", "sharedLink.update",

View File

@ -268,7 +268,7 @@ export type CreateAlbumDto = {
assetIds?: string[]; assetIds?: string[];
description?: string; description?: string;
}; };
export type AlbumCountResponseDto = { export type AlbumStatisticsResponseDto = {
notShared: number; notShared: number;
owned: number; owned: number;
shared: number; shared: number;
@ -869,6 +869,15 @@ export type ServerFeaturesDto = {
smartSearch: boolean; smartSearch: boolean;
trash: boolean; trash: boolean;
}; };
export type LicenseResponseDto = {
activatedAt: string;
activationKey: string;
licenseKey: string;
};
export type LicenseKeyDto = {
activationKey: string;
licenseKey: string;
};
export type ServerMediaTypesResponseDto = { export type ServerMediaTypesResponseDto = {
image: string[]; image: string[];
sidecar: string[]; sidecar: string[];
@ -909,15 +918,6 @@ export type ServerVersionResponseDto = {
minor: number; minor: number;
patch: number; patch: number;
}; };
export type LicenseResponseDto = {
activatedAt: string;
activationKey: string;
licenseKey: string;
};
export type LicenseKeyDto = {
activationKey: string;
licenseKey: string;
};
export type SessionResponseDto = { export type SessionResponseDto = {
createdAt: string; createdAt: string;
current: boolean; current: boolean;
@ -1369,11 +1369,11 @@ export function createAlbum({ createAlbumDto }: {
body: createAlbumDto body: createAlbumDto
}))); })));
} }
export function getAlbumCount(opts?: Oazapfts.RequestOpts) { export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: AlbumCountResponseDto; data: AlbumStatisticsResponseDto;
}>("/albums/count", { }>("/albums/statistics", {
...opts ...opts
})); }));
} }
@ -2168,7 +2168,7 @@ export function redirectOAuthToMobile(opts?: Oazapfts.RequestOpts) {
} }
export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) { export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 201; status: 200;
data: UserAdminResponseDto; data: UserAdminResponseDto;
}>("/oauth/unlink", { }>("/oauth/unlink", {
...opts, ...opts,
@ -2458,102 +2458,27 @@ export function getSearchSuggestions({ country, includeNull, make, model, state,
...opts ...opts
})); }));
} }
/**
* This property was deprecated in v1.107.0
*/
export function getAboutInfo(opts?: Oazapfts.RequestOpts) { export function getAboutInfo(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: ServerAboutResponseDto; data: ServerAboutResponseDto;
}>("/server-info/about", { }>("/server/about", {
...opts ...opts
})); }));
} }
/**
* This property was deprecated in v1.107.0
*/
export function getServerConfig(opts?: Oazapfts.RequestOpts) { export function getServerConfig(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: ServerConfigDto; data: ServerConfigDto;
}>("/server-info/config", { }>("/server/config", {
...opts ...opts
})); }));
} }
/**
* This property was deprecated in v1.107.0
*/
export function getServerFeatures(opts?: Oazapfts.RequestOpts) { export function getServerFeatures(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: ServerFeaturesDto; data: ServerFeaturesDto;
}>("/server-info/features", { }>("/server/features", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerMediaTypesResponseDto;
}>("/server-info/media-types", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function pingServer(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerPingResponseRead;
}>("/server-info/ping", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function getServerStatistics(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerStatsResponseDto;
}>("/server-info/statistics", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function getStorage(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerStorageResponseDto;
}>("/server-info/storage", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function getTheme(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerThemeDto;
}>("/server-info/theme", {
...opts
}));
}
/**
* This property was deprecated in v1.107.0
*/
export function getServerVersion(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerVersionResponseDto;
}>("/server-info/version", {
...opts ...opts
})); }));
} }
@ -2585,6 +2510,54 @@ export function setServerLicense({ licenseKeyDto }: {
body: licenseKeyDto body: licenseKeyDto
}))); })));
} }
export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerMediaTypesResponseDto;
}>("/server/media-types", {
...opts
}));
}
export function pingServer(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerPingResponseRead;
}>("/server/ping", {
...opts
}));
}
export function getServerStatistics(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerStatsResponseDto;
}>("/server/statistics", {
...opts
}));
}
export function getStorage(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerStorageResponseDto;
}>("/server/storage", {
...opts
}));
}
export function getTheme(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerThemeDto;
}>("/server/theme", {
...opts
}));
}
export function getServerVersion(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerVersionResponseDto;
}>("/server/version", {
...opts
}));
}
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/sessions", { return oazapfts.ok(oazapfts.fetchText("/sessions", {
...opts, ...opts,
@ -3205,7 +3178,6 @@ export enum Permission {
AssetRead = "asset.read", AssetRead = "asset.read",
AssetUpdate = "asset.update", AssetUpdate = "asset.update",
AssetDelete = "asset.delete", AssetDelete = "asset.delete",
AssetRestore = "asset.restore",
AssetShare = "asset.share", AssetShare = "asset.share",
AssetView = "asset.view", AssetView = "asset.view",
AssetDownload = "asset.download", AssetDownload = "asset.download",
@ -3247,6 +3219,9 @@ export enum Permission {
PersonStatistics = "person.statistics", PersonStatistics = "person.statistics",
PersonMerge = "person.merge", PersonMerge = "person.merge",
PersonReassign = "person.reassign", PersonReassign = "person.reassign",
SessionRead = "session.read",
SessionUpdate = "session.update",
SessionDelete = "session.delete",
SharedLinkCreate = "sharedLink.create", SharedLinkCreate = "sharedLink.create",
SharedLinkRead = "sharedLink.read", SharedLinkRead = "sharedLink.read",
SharedLinkUpdate = "sharedLink.update", SharedLinkUpdate = "sharedLink.update",

View File

@ -79,7 +79,11 @@
"schedule": "on tuesday" "schedule": "on tuesday"
} }
], ],
"ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"], "ignorePaths": [
"mobile/openapi/pubspec.yaml",
"mobile/ios",
"mobile/android"
],
"ignoreDeps": ["http", "intl"], "ignoreDeps": ["http", "intl"],
"labels": ["dependencies"] "labels": ["dependencies", "changelog:skip"]
} }

View File

@ -25,12 +25,6 @@ export class ActivityController {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@Get('statistics')
@Authenticated({ permission: Permission.ACTIVITY_STATISTICS })
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Post() @Post()
@Authenticated({ permission: Permission.ACTIVITY_CREATE }) @Authenticated({ permission: Permission.ACTIVITY_CREATE })
async createActivity( async createActivity(
@ -45,6 +39,12 @@ export class ActivityController {
return value; return value;
} }
@Get('statistics')
@Authenticated({ permission: Permission.ACTIVITY_STATISTICS })
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.ACTIVITY_DELETE }) @Authenticated({ permission: Permission.ACTIVITY_DELETE })

View File

@ -2,9 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { import {
AddUsersDto, AddUsersDto,
AlbumCountResponseDto,
AlbumInfoDto, AlbumInfoDto,
AlbumResponseDto, AlbumResponseDto,
AlbumStatisticsResponseDto,
CreateAlbumDto, CreateAlbumDto,
GetAlbumsDto, GetAlbumsDto,
UpdateAlbumDto, UpdateAlbumDto,
@ -22,12 +22,6 @@ import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
export class AlbumController { export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get('count')
@Authenticated({ permission: Permission.ALBUM_STATISTICS })
getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(auth);
}
@Get() @Get()
@Authenticated({ permission: Permission.ALBUM_READ }) @Authenticated({ permission: Permission.ALBUM_READ })
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> { getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
@ -40,6 +34,12 @@ export class AlbumController {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get('statistics')
@Authenticated({ permission: Permission.ALBUM_STATISTICS })
getAlbumStatistics(@Auth() auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
return this.service.getStatistics(auth);
}
@Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true })
@Get(':id') @Get(':id')
getAlbumInfo( getAlbumInfo(

View File

@ -93,8 +93,8 @@ export class AssetMediaController {
@Put(':id/original') @Put(':id/original')
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@Authenticated({ sharedLink: true })
@EndpointLifecycle({ addedAt: 'v1.106.0' }) @EndpointLifecycle({ addedAt: 'v1.106.0' })
@Authenticated({ sharedLink: true })
async replaceAsset( async replaceAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View File

@ -51,8 +51,8 @@ export class AssetController {
} }
@Post('jobs') @Post('jobs')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> { runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(auth, dto); return this.service.run(auth, dto);
} }

View File

@ -31,18 +31,18 @@ export class LibraryController {
return this.service.create(dto); return this.service.create(dto);
} }
@Put(':id')
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true })
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto);
}
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) @Authenticated({ permission: Permission.LIBRARY_READ, admin: true })
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> { getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Put(':id')
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true })
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto);
}
@Post(':id/validate') @Post(':id/validate')
@HttpCode(200) @HttpCode(200)
@Authenticated({ admin: true }) @Authenticated({ admin: true })

View File

@ -1,4 +1,4 @@
import { Body, Controller, HttpCode, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
@ -11,7 +11,7 @@ export class NotificationController {
constructor(private service: NotificationService) {} constructor(private service: NotificationService) {}
@Post('test-email') @Post('test-email')
@HttpCode(200) @HttpCode(HttpStatus.OK)
@Authenticated({ admin: true }) @Authenticated({ admin: true })
sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) { sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) {
return this.service.sendTestEmail(auth.user.id, dto); return this.service.sendTestEmail(auth.user.id, dto);

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { AuthType } from 'src/constants'; import { AuthType } from 'src/constants';
@ -58,6 +58,7 @@ export class OAuthController {
} }
@Post('unlink') @Post('unlink')
@HttpCode(HttpStatus.OK)
@Authenticated() @Authenticated()
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> { unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
return this.service.unlink(auth); return this.service.unlink(auth);

View File

@ -1,9 +1,8 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -14,9 +13,7 @@ export class PartnerController {
constructor(private service: PartnerService) {} constructor(private service: PartnerService) {}
@Get() @Get()
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
@Authenticated({ permission: Permission.PARTNER_READ }) @Authenticated({ permission: Permission.PARTNER_READ })
// TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }

View File

@ -1,5 +1,5 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiExcludeController, ApiTags } from '@nestjs/swagger';
import { EndpointLifecycle } from 'src/decorators'; import { EndpointLifecycle } from 'src/decorators';
import { import {
ServerAboutResponseDto, ServerAboutResponseDto,
@ -16,6 +16,7 @@ import { Authenticated } from 'src/middleware/auth.guard';
import { ServerService } from 'src/services/server.service'; import { ServerService } from 'src/services/server.service';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
@ApiExcludeController()
@ApiTags('Server Info') @ApiTags('Server Info')
@Controller('server-info') @Controller('server-info')
export class ServerInfoController { export class ServerInfoController {
@ -68,9 +69,9 @@ export class ServerInfoController {
return this.service.getConfig(); return this.service.getConfig();
} }
@Authenticated({ admin: true })
@EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
@Get('statistics') @Get('statistics')
@EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
@Authenticated({ admin: true })
getServerStatistics(): Promise<ServerStatsResponseDto> { getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics(); return this.service.getStatistics();
} }

View File

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Put } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { import {
ServerAboutResponseDto, ServerAboutResponseDto,
@ -26,57 +26,48 @@ export class ServerController {
@Get('about') @Get('about')
@Authenticated() @Authenticated()
@ApiExcludeEndpoint()
getAboutInfo(): Promise<ServerAboutResponseDto> { getAboutInfo(): Promise<ServerAboutResponseDto> {
return this.service.getAboutInfo(); return this.service.getAboutInfo();
} }
@Get('storage') @Get('storage')
@Authenticated() @Authenticated()
@ApiExcludeEndpoint()
getStorage(): Promise<ServerStorageResponseDto> { getStorage(): Promise<ServerStorageResponseDto> {
return this.service.getStorage(); return this.service.getStorage();
} }
@Get('ping') @Get('ping')
@ApiExcludeEndpoint()
pingServer(): ServerPingResponse { pingServer(): ServerPingResponse {
return this.service.ping(); return this.service.ping();
} }
@Get('version') @Get('version')
@ApiExcludeEndpoint()
getServerVersion(): ServerVersionResponseDto { getServerVersion(): ServerVersionResponseDto {
return this.versionService.getVersion(); return this.versionService.getVersion();
} }
@Get('features') @Get('features')
@ApiExcludeEndpoint()
getServerFeatures(): Promise<ServerFeaturesDto> { getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures(); return this.service.getFeatures();
} }
@Get('theme') @Get('theme')
@ApiExcludeEndpoint()
getTheme(): Promise<ServerThemeDto> { getTheme(): Promise<ServerThemeDto> {
return this.service.getTheme(); return this.service.getTheme();
} }
@Get('config') @Get('config')
@ApiExcludeEndpoint()
getServerConfig(): Promise<ServerConfigDto> { getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig(); return this.service.getConfig();
} }
@Authenticated({ admin: true })
@Get('statistics') @Get('statistics')
@ApiExcludeEndpoint() @Authenticated({ admin: true })
getServerStatistics(): Promise<ServerStatsResponseDto> { getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics(); return this.service.getStatistics();
} }
@Get('media-types') @Get('media-types')
@ApiExcludeEndpoint()
getSupportedMediaTypes(): ServerMediaTypesResponseDto { getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes(); return this.service.getSupportedMediaTypes();
} }

View File

@ -2,6 +2,7 @@ import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/co
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto } from 'src/dtos/session.dto'; import { SessionResponseDto } from 'src/dtos/session.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -12,21 +13,21 @@ export class SessionController {
constructor(private service: SessionService) {} constructor(private service: SessionService) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: Permission.SESSION_READ })
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> { getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Delete() @Delete()
@Authenticated({ permission: Permission.SESSION_DELETE })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated()
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> { deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth); return this.service.deleteAll(auth);
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.SESSION_DELETE })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated()
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -26,8 +26,8 @@ export class StackController {
} }
@Delete() @Delete()
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.STACK_DELETE }) @Authenticated({ permission: Permission.STACK_DELETE })
@HttpCode(HttpStatus.NO_CONTENT)
deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }

View File

@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TimelineService } from 'src/services/timeline.service'; import { TimelineService } from 'src/services/timeline.service';
@ -11,14 +12,14 @@ import { TimelineService } from 'src/services/timeline.service';
export class TimelineController { export class TimelineController {
constructor(private service: TimelineService) {} constructor(private service: TimelineService) {}
@Authenticated({ sharedLink: true })
@Get('buckets') @Get('buckets')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> { getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(auth, dto); return this.service.getTimeBuckets(auth, dto);
} }
@Authenticated({ sharedLink: true })
@Get('bucket') @Get('bucket')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>; return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
} }

View File

@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
@ -12,21 +13,21 @@ export class TrashController {
@Post('empty') @Post('empty')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: Permission.ASSET_DELETE })
emptyTrash(@Auth() auth: AuthDto): Promise<void> { emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.empty(auth); return this.service.empty(auth);
} }
@Post('restore') @Post('restore')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: Permission.ASSET_DELETE })
restoreTrash(@Auth() auth: AuthDto): Promise<void> { restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.restore(auth); return this.service.restore(auth);
} }
@Post('restore/assets') @Post('restore/assets')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: Permission.ASSET_DELETE })
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAssets(auth, dto); return this.service.restoreAssets(auth, dto);
} }

View File

@ -1,312 +0,0 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { AlbumUserRole, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
let instance: AccessCore | null;
export class AccessCore {
private constructor(private repository: IAccessRepository) {}
static create(repository: IAccessRepository) {
if (!instance) {
instance = new AccessCore(repository);
}
return instance;
}
static reset() {
instance = null;
}
requireUploadAccess(auth: AuthDto | null): AuthDto {
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
throw new UnauthorizedException();
}
return auth;
}
/**
* Check if user has access to all ids, for the given permission.
* Throws error if user does not have access to any of the ids.
*/
async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) {
ids = Array.isArray(ids) ? ids : [ids];
const allowedIds = await this.checkAccess(auth, permission, ids);
if (!setIsEqual(new Set(ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${permission} access`);
}
}
/**
* Return ids that user has access to, for the given permission.
* Check is done for each id, and only allowed ids are returned.
*
* @returns Set<string>
*/
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]): Promise<Set<string>> {
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) {
return new Set();
}
if (auth.sharedLink) {
return this.checkAccessSharedLink(auth.sharedLink, permission, idSet);
}
return this.checkAccessOther(auth, permission, idSet);
}
private async checkAccessSharedLink(
sharedLink: SharedLinkEntity,
permission: Permission,
ids: Set<string>,
): Promise<Set<string>> {
const sharedLinkId = sharedLink.id;
switch (permission) {
case Permission.ASSET_READ: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_VIEW: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_DOWNLOAD: {
return sharedLink.allowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
}
case Permission.ASSET_UPLOAD: {
return sharedLink.allowUpload ? ids : new Set();
}
case Permission.ASSET_SHARE: {
// TODO: fix this to not use sharedLink.userId for access control
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
}
case Permission.ALBUM_READ: {
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ALBUM_DOWNLOAD: {
return sharedLink.allowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
}
case Permission.ALBUM_ADD_ASSET: {
return sharedLink.allowUpload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
}
default: {
return new Set();
}
}
}
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>): Promise<Set<string>> {
switch (permission) {
// uses album id
case Permission.ACTIVITY_CREATE: {
return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
}
// uses activity id
case Permission.ACTIVITY_DELETE: {
const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids);
const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess(
auth.user.id,
setDifference(ids, isOwner),
);
return setUnion(isOwner, isAlbumOwner);
}
case Permission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_SHARE: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.ASSET_VIEW: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_DOWNLOAD: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_UPDATE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_DELETE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_RESTORE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_READ: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.VIEWER,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_ADD_ASSET: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.EDITOR,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_UPDATE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DELETE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_SHARE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DOWNLOAD: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.VIEWER,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_REMOVE_ASSET: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.EDITOR,
);
return setUnion(isOwner, isShared);
}
case Permission.ASSET_UPLOAD: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
}
case Permission.ARCHIVE_READ: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.AUTH_DEVICE_DELETE: {
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
}
case Permission.TIMELINE_READ: {
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.TIMELINE_DOWNLOAD: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.MEMORY_READ: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_UPDATE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_DELETE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_DELETE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_READ: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_UPDATE: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_MERGE: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_CREATE: {
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_REASSIGN: {
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PARTNER_UPDATE: {
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
}
case Permission.STACK_READ: {
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
}
case Permission.STACK_UPDATE: {
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
}
case Permission.STACK_DELETE: {
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
}
default: {
return new Set();
}
}
}
}

View File

@ -95,7 +95,7 @@ export class GetAlbumsDto {
assetId?: string; assetId?: string;
} }
export class AlbumCountResponseDto { export class AlbumStatisticsResponseDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
owned!: number; owned!: number;

View File

@ -54,7 +54,6 @@ export enum Permission {
ASSET_READ = 'asset.read', ASSET_READ = 'asset.read',
ASSET_UPDATE = 'asset.update', ASSET_UPDATE = 'asset.update',
ASSET_DELETE = 'asset.delete', ASSET_DELETE = 'asset.delete',
ASSET_RESTORE = 'asset.restore',
ASSET_SHARE = 'asset.share', ASSET_SHARE = 'asset.share',
ASSET_VIEW = 'asset.view', ASSET_VIEW = 'asset.view',
ASSET_DOWNLOAD = 'asset.download', ASSET_DOWNLOAD = 'asset.download',
@ -107,6 +106,10 @@ export enum Permission {
PERSON_MERGE = 'person.merge', PERSON_MERGE = 'person.merge',
PERSON_REASSIGN = 'person.reassign', PERSON_REASSIGN = 'person.reassign',
SESSION_READ = 'session.read',
SESSION_UPDATE = 'session.update',
SESSION_DELETE = 'session.delete',
SHARED_LINK_CREATE = 'sharedLink.create', SHARED_LINK_CREATE = 'sharedLink.create',
SHARED_LINK_READ = 'sharedLink.read', SHARED_LINK_READ = 'sharedLink.read',
SHARED_LINK_UPDATE = 'sharedLink.update', SHARED_LINK_UPDATE = 'sharedLink.update',

View File

@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { import {
ActivityCreateDto, ActivityCreateDto,
ActivityDto, ActivityDto,
@ -16,20 +15,17 @@ import { ActivityEntity } from 'src/entities/activity.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IActivityRepository } from 'src/interfaces/activity.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface';
import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class ActivityService { export class ActivityService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IActivityRepository) private repository: IActivityRepository, @Inject(IActivityRepository) private repository: IActivityRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
const activities = await this.repository.search({ const activities = await this.repository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
@ -41,12 +37,12 @@ export class ActivityService {
} }
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
} }
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> { async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId); await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
const common = { const common = {
userId: auth.user.id, userId: auth.user.id,
@ -80,7 +76,7 @@ export class ActivityService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id); await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
await this.repository.delete(id); await this.repository.delete(id);
} }
} }

View File

@ -43,12 +43,12 @@ describe(AlbumService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
describe('getCount', () => { describe('getStatistics', () => {
it('should get the album count', async () => { it('should get the album count', async () => {
albumMock.getOwned.mockResolvedValue([]); albumMock.getOwned.mockResolvedValue([]);
albumMock.getShared.mockResolvedValue([]); albumMock.getShared.mockResolvedValue([]);
albumMock.getNotShared.mockResolvedValue([]); albumMock.getNotShared.mockResolvedValue([]);
await expect(sut.getCount(authStub.admin)).resolves.toEqual({ await expect(sut.getStatistics(authStub.admin)).resolves.toEqual({
owned: 0, owned: 0,
shared: 0, shared: 0,
notShared: 0, notShared: 0,

View File

@ -1,10 +1,9 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { import {
AddUsersDto, AddUsersDto,
AlbumCountResponseDto,
AlbumInfoDto, AlbumInfoDto,
AlbumResponseDto, AlbumResponseDto,
AlbumStatisticsResponseDto,
CreateAlbumDto, CreateAlbumDto,
GetAlbumsDto, GetAlbumsDto,
UpdateAlbumDto, UpdateAlbumDto,
@ -24,23 +23,21 @@ import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfa
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
@Injectable() @Injectable()
export class AlbumService { export class AlbumService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) private accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async getCount(auth: AuthDto): Promise<AlbumCountResponseDto> { async getStatistics(auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
const [owned, shared, notShared] = await Promise.all([ const [owned, shared, notShared] = await Promise.all([
this.albumRepository.getOwned(auth.user.id), this.albumRepository.getOwned(auth.user.id),
this.albumRepository.getShared(auth.user.id), this.albumRepository.getShared(auth.user.id),
@ -102,7 +99,7 @@ export class AlbumService {
} }
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] });
await this.albumRepository.updateThumbnails(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets }); const album = await this.findOrFail(id, { withAssets });
@ -126,7 +123,11 @@ export class AlbumService {
} }
} }
const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds)); const allowedAssetIdsSet = await checkAccess(this.access, {
auth,
permission: Permission.ASSET_SHARE,
ids: dto.assetIds || [],
});
const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity); const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
const album = await this.albumRepository.create({ const album = await this.albumRepository.create({
@ -146,7 +147,7 @@ export class AlbumService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
const album = await this.findOrFail(id, { withAssets: true }); const album = await this.findOrFail(id, { withAssets: true });
@ -169,17 +170,17 @@ export class AlbumService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] });
await this.albumRepository.delete(id); await this.albumRepository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
const results = await addAssets( const results = await addAssets(
auth, auth,
{ accessRepository: this.accessRepository, repository: this.albumRepository }, { access: this.access, bulk: this.albumRepository },
{ parentId: id, assetIds: dto.ids }, { parentId: id, assetIds: dto.ids },
); );
@ -198,12 +199,12 @@ export class AlbumService {
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
const results = await removeAssets( const results = await removeAssets(
auth, auth,
{ accessRepository: this.accessRepository, repository: this.albumRepository }, { access: this.access, bulk: this.albumRepository },
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE },
); );
@ -219,7 +220,7 @@ export class AlbumService {
} }
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> { async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
@ -263,15 +264,14 @@ export class AlbumService {
// non-admin can remove themselves // non-admin can remove themselves
if (auth.user.id !== userId) { if (auth.user.id !== userId) {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
} }
await this.albumUserRepository.delete({ albumId: id, userId }); await this.albumUserRepository.delete({ albumId: id, userId });
} }
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> { async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
} }

View File

@ -7,7 +7,6 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { extname } from 'node:path'; import { extname } from 'node:path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { AccessCore } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { import {
AssetBulkUploadCheckResponseDto, AssetBulkUploadCheckResponseDto,
@ -36,6 +35,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { requireAccess, requireUploadAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
@ -57,10 +57,8 @@ export interface UploadFile {
@Injectable() @Injectable()
export class AssetMediaService { export class AssetMediaService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
@ -69,7 +67,6 @@ export class AssetMediaService {
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
) { ) {
this.logger.setContext(AssetMediaService.name); this.logger.setContext(AssetMediaService.name);
this.access = AccessCore.create(accessRepository);
} }
async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> { async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> {
@ -86,7 +83,7 @@ export class AssetMediaService {
} }
canUploadFile({ auth, fieldName, file }: UploadRequest): true { canUploadFile({ auth, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(auth); requireUploadAccess(auth);
const filename = file.originalName; const filename = file.originalName;
@ -118,7 +115,7 @@ export class AssetMediaService {
} }
getUploadFilename({ auth, fieldName, file }: UploadRequest): string { getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
this.access.requireUploadAccess(auth); requireUploadAccess(auth);
const originalExtension = extname(file.originalName); const originalExtension = extname(file.originalName);
@ -132,7 +129,7 @@ export class AssetMediaService {
} }
getUploadFolder({ auth, fieldName, file }: UploadRequest): string { getUploadFolder({ auth, fieldName, file }: UploadRequest): string {
auth = this.access.requireUploadAccess(auth); auth = requireUploadAccess(auth);
let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid);
if (fieldName === UploadFieldName.PROFILE_DATA) { if (fieldName === UploadFieldName.PROFILE_DATA) {
@ -151,12 +148,12 @@ export class AssetMediaService {
sidecarFile?: UploadFile, sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
try { try {
await this.access.requirePermission( await requireAccess(this.access, {
auth, auth,
Permission.ASSET_UPLOAD, permission: Permission.ASSET_UPLOAD,
// do not need an id here, but the interface requires it // do not need an id here, but the interface requires it
auth.user.id, ids: [auth.user.id],
); });
this.requireQuota(auth, file.size); this.requireQuota(auth, file.size);
@ -195,7 +192,7 @@ export class AssetMediaService {
sidecarFile?: UploadFile, sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
try { try {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
const asset = (await this.assetRepository.getById(id)) as AssetEntity; const asset = (await this.assetRepository.getById(id)) as AssetEntity;
this.requireQuota(auth, file.size); this.requireQuota(auth, file.size);
@ -219,7 +216,7 @@ export class AssetMediaService {
} }
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
if (!asset) { if (!asset) {
@ -234,7 +231,7 @@ export class AssetMediaService {
} }
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> { async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
const size = dto.size ?? AssetMediaSize.THUMBNAIL; const size = dto.size ?? AssetMediaSize.THUMBNAIL;
@ -257,7 +254,7 @@ export class AssetMediaService {
} }
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
if (!asset) { if (!asset) {

View File

@ -1,7 +1,6 @@
import { BadRequestException, Inject } from '@nestjs/common'; import { BadRequestException, Inject } from '@nestjs/common';
import _ from 'lodash'; import _ from 'lodash';
import { DateTime, Duration } from 'luxon'; import { DateTime, Duration } from 'luxon';
import { AccessCore } from 'src/cores/access.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { import {
AssetResponseDto, AssetResponseDto,
@ -39,15 +38,15 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { requireAccess } from 'src/utils/access';
import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
export class AssetService { export class AssetService {
private access: AccessCore;
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@ -58,7 +57,6 @@ export class AssetService {
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
) { ) {
this.logger.setContext(AssetService.name); this.logger.setContext(AssetService.name);
this.access = AccessCore.create(accessRepository);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
} }
@ -109,7 +107,7 @@ export class AssetService {
} }
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> { async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] });
const asset = await this.assetRepository.getById( const asset = await this.assetRepository.getById(
id, id,
@ -158,7 +156,7 @@ export class AssetService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
@ -182,7 +180,7 @@ export class AssetService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids });
for (const id of ids) { for (const id of ids) {
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
@ -278,7 +276,7 @@ export class AssetService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto; const { ids, force } = dto;
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
if (force) { if (force) {
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
@ -294,7 +292,7 @@ export class AssetService {
} }
async run(auth: AuthDto, dto: AssetJobsDto) { async run(auth: AuthDto, dto: AssetJobsDto) {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
const jobs: JobItem[] = []; const jobs: JobItem[] = [];

View File

@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { import {
AuditDeletesDto, AuditDeletesDto,
@ -24,15 +23,14 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
export class AuditService { export class AuditService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository,
@ -41,7 +39,6 @@ export class AuditService {
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
) { ) {
this.access = AccessCore.create(accessRepository);
this.logger.setContext(AuditService.name); this.logger.setContext(AuditService.name);
} }
@ -52,7 +49,7 @@ export class AuditService {
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || auth.user.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
const audits = await this.repository.getAfter(dto.after, { const audits = await this.repository.getAfter(dto.after, {
userIds: [userId], userIds: [userId],

View File

@ -1,6 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { parse } from 'node:path'; import { parse } from 'node:path';
import { AccessCore } from 'src/cores/access.core';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -11,21 +10,19 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
import { requireAccess } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
export class DownloadService { export class DownloadService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
) { ) {
this.access = AccessCore.create(accessRepository);
this.logger.setContext(DownloadService.name); this.logger.setContext(DownloadService.name);
} }
@ -76,7 +73,7 @@ export class DownloadService {
} }
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> { async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds); await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
const zip = this.storageRepository.createZipStream(); const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds); const assets = await this.assetRepository.getByIds(dto.assetIds);
@ -119,20 +116,20 @@ export class DownloadService {
if (dto.assetIds) { if (dto.assetIds) {
const assetIds = dto.assetIds; const assetIds = dto.assetIds;
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
} }
if (dto.albumId) { if (dto.albumId) {
const albumId = dto.albumId; const albumId = dto.albumId;
await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId); await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
} }
if (dto.userId) { if (dto.userId) {
const userId = dto.userId; const userId = dto.userId;
await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId); await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
return usePagination(PAGINATION_SIZE, (pagination) => return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
); );

View File

@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
@ -7,18 +6,15 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
@Injectable() @Injectable()
export class MemoryService { export class MemoryService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) private accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IMemoryRepository) private repository: IMemoryRepository, @Inject(IMemoryRepository) private repository: IMemoryRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async search(auth: AuthDto) { async search(auth: AuthDto) {
const memories = await this.repository.search(auth.user.id); const memories = await this.repository.search(auth.user.id);
@ -26,7 +22,7 @@ export class MemoryService {
} }
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> { async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
await this.access.requirePermission(auth, Permission.MEMORY_READ, id); await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
const memory = await this.findOrFail(id); const memory = await this.findOrFail(id);
return mapMemory(memory); return mapMemory(memory);
} }
@ -35,7 +31,11 @@ export class MemoryService {
// TODO validate type/data combination // TODO validate type/data combination
const assetIds = dto.assetIds || []; const assetIds = dto.assetIds || [];
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); const allowedAssetIds = await checkAccess(this.access, {
auth,
permission: Permission.ASSET_SHARE,
ids: assetIds,
});
const memory = await this.repository.create({ const memory = await this.repository.create({
ownerId: auth.user.id, ownerId: auth.user.id,
type: dto.type, type: dto.type,
@ -50,7 +50,7 @@ export class MemoryService {
} }
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> { async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
const memory = await this.repository.update({ const memory = await this.repository.update({
id, id,
@ -63,14 +63,14 @@ export class MemoryService {
} }
async remove(auth: AuthDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
await this.repository.delete(id); await this.repository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.access.requirePermission(auth, Permission.MEMORY_READ, id); await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
const repos = { accessRepository: this.accessRepository, repository: this.repository }; const repos = { access: this.access, bulk: this.repository };
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
@ -82,9 +82,9 @@ export class MemoryService {
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
const repos = { accessRepository: this.accessRepository, repository: this.repository }; const repos = { access: this.access, bulk: this.repository };
const results = await removeAssets(auth, repos, { const results = await removeAssets(auth, repos, {
parentId: id, parentId: id,
assetIds: dto.ids, assetIds: dto.ids,

View File

@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto'; import { mapUser } from 'src/dtos/user.dto';
@ -7,16 +6,14 @@ import { PartnerEntity } from 'src/entities/partner.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class PartnerService { export class PartnerService {
private access: AccessCore;
constructor( constructor(
@Inject(IPartnerRepository) private repository: IPartnerRepository, @Inject(IPartnerRepository) private repository: IPartnerRepository,
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> { async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
@ -49,7 +46,7 @@ export class PartnerService {
} }
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> { async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById); await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });

View File

@ -1,7 +1,6 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { ImageFormat } from 'src/config'; import { ImageFormat } from 'src/config';
import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { AccessCore } from 'src/cores/access.core';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
@ -50,6 +49,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
@ -59,12 +59,11 @@ import { IsNull } from 'typeorm';
@Injectable() @Injectable()
export class PersonService { export class PersonService {
private access: AccessCore;
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
private storageCore: StorageCore; private storageCore: StorageCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository,
@ -77,7 +76,6 @@ export class PersonService {
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
) { ) {
this.access = AccessCore.create(accessRepository);
this.logger.setContext(PersonService.name); this.logger.setContext(PersonService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
this.storageCore = StorageCore.create( this.storageCore = StorageCore.create(
@ -114,7 +112,7 @@ export class PersonService {
} }
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> { async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
const result: PersonResponseDto[] = []; const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = []; const changeFeaturePhoto: string[] = [];
@ -122,7 +120,7 @@ export class PersonService {
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
for (const face of faces) { for (const face of faces) {
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id); await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
if (person.faceAssetId === null) { if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id); changeFeaturePhoto.push(person.id);
} }
@ -143,9 +141,8 @@ export class PersonService {
} }
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> { async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id);
const face = await this.repository.getFaceById(dto.id); const face = await this.repository.getFaceById(dto.id);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
@ -161,7 +158,7 @@ export class PersonService {
} }
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> { async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id); await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
const faces = await this.repository.getFaces(dto.id); const faces = await this.repository.getFaces(dto.id);
return faces.map((asset) => mapFaces(asset, auth)); return faces.map((asset) => mapFaces(asset, auth));
} }
@ -188,17 +185,17 @@ export class PersonService {
} }
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> { async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
return this.findOrFail(id).then(mapPerson); return this.findOrFail(id).then(mapPerson);
} }
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> { async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
return this.repository.getStatistics(id); return this.repository.getStatistics(id);
} }
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
const person = await this.repository.getById(id); const person = await this.repository.getById(id);
if (!person || !person.thumbnailPath) { if (!person || !person.thumbnailPath) {
throw new NotFoundException(); throw new NotFoundException();
@ -212,7 +209,7 @@ export class PersonService {
} }
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> { async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
const assets = await this.repository.getAssets(id); const assets = await this.repository.getAssets(id);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
@ -227,13 +224,13 @@ export class PersonService {
} }
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
// TODO: set by faceId directly // TODO: set by faceId directly
let faceId: string | undefined = undefined; let faceId: string | undefined = undefined;
if (assetId) { if (assetId) {
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
if (!face) { if (!face) {
throw new BadRequestException('Invalid assetId for feature face'); throw new BadRequestException('Invalid assetId for feature face');
@ -587,13 +584,17 @@ export class PersonService {
throw new BadRequestException('Cannot merge a person into themselves'); throw new BadRequestException('Cannot merge a person into themselves');
} }
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
let primaryPerson = await this.findOrFail(id); let primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id; const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds); const allowedIds = await checkAccess(this.access, {
auth,
permission: Permission.PERSON_MERGE,
ids: mergeIds,
});
for (const mergeId of mergeIds) { for (const mergeId of mergeIds) {
const hasAccess = allowedIds.has(mergeId); const hasAccess = allowedIds.has(mergeId);

View File

@ -1,6 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AccessCore } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
@ -8,18 +7,16 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
import { JobStatus } from 'src/interfaces/job.interface'; import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class SessionService { export class SessionService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository,
) { ) {
this.logger.setContext(SessionService.name); this.logger.setContext(SessionService.name);
this.access = AccessCore.create(accessRepository);
} }
async handleCleanup() { async handleCleanup() {
@ -47,7 +44,7 @@ export class SessionService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
await this.sessionRepository.delete(id); await this.sessionRepository.delete(id);
} }

View File

@ -1,6 +1,5 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { AccessCore } from 'src/cores/access.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
@ -21,22 +20,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { OpenGraphTags } from 'src/utils/misc'; import { OpenGraphTags } from 'src/utils/misc';
@Injectable() @Injectable()
export class SharedLinkService { export class SharedLinkService {
private access: AccessCore;
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
) { ) {
this.logger.setContext(SharedLinkService.name); this.logger.setContext(SharedLinkService.name);
this.access = AccessCore.create(accessRepository);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
} }
@ -69,7 +67,7 @@ export class SharedLinkService {
if (!dto.albumId) { if (!dto.albumId) {
throw new BadRequestException('Invalid albumId'); throw new BadRequestException('Invalid albumId');
} }
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
break; break;
} }
@ -78,7 +76,7 @@ export class SharedLinkService {
throw new BadRequestException('Invalid assetIds'); throw new BadRequestException('Invalid assetIds');
} }
await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
break; break;
} }
@ -139,7 +137,11 @@ export class SharedLinkService {
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); const allowedAssetIds = await checkAccess(this.access, {
auth,
permission: Permission.ASSET_SHARE,
ids: notPresentAssetIds,
});
const results: AssetIdsResponseDto[] = []; const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {

View File

@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
@ -7,18 +6,15 @@ import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class StackService { export class StackService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository, @Inject(IStackRepository) private stackRepository: IStackRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> { async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> {
const stacks = await this.stackRepository.search({ const stacks = await this.stackRepository.search({
@ -30,7 +26,7 @@ export class StackService {
} }
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> { async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
@ -40,13 +36,13 @@ export class StackService {
} }
async get(auth: AuthDto, id: string): Promise<StackResponseDto> { async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
await this.access.requirePermission(auth, Permission.STACK_READ, id); await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] });
const stack = await this.findOrFail(id); const stack = await this.findOrFail(id);
return mapStack(stack, { auth }); return mapStack(stack, { auth });
} }
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> { async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
await this.access.requirePermission(auth, Permission.STACK_UPDATE, id); await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] });
const stack = await this.findOrFail(id); const stack = await this.findOrFail(id);
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack'); throw new BadRequestException('Primary asset must be in the stack');
@ -60,14 +56,14 @@ export class StackService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.STACK_DELETE, id); await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] });
await this.stackRepository.delete(id); await this.stackRepository.delete(id);
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []);
} }
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> { async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
await this.access.requirePermission(auth, Permission.STACK_DELETE, dto.ids); await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids });
await this.stackRepository.deleteAll(dto.ids); await this.stackRepository.deleteAll(dto.ids);
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []);

View File

@ -1,7 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore } from 'src/cores/access.core';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
@ -10,27 +9,24 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { requireAccess } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
import { setIsEqual } from 'src/utils/set'; import { setIsEqual } from 'src/utils/set';
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
export class SyncService { export class SyncService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IAuditRepository) private auditRepository: IAuditRepository, @Inject(IAuditRepository) private auditRepository: IAuditRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
// mobile implementation is faster if this is a single id // mobile implementation is faster if this is a single id
const userId = dto.userId || auth.user.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
const assets = await this.assetRepository.getAllForUserFullSync({ const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId, ownerId: userId,
updatedUntil: dto.updatedUntil, updatedUntil: dto.updatedUntil,
@ -54,7 +50,7 @@ export class SyncService {
return FULL_SYNC; return FULL_SYNC;
} }
await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
const limit = 10_000; const limit = 10_000;
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });

View File

@ -1,5 +1,4 @@
import { BadRequestException, Inject } from '@nestjs/common'; import { BadRequestException, Inject } from '@nestjs/common';
import { AccessCore } from 'src/cores/access.core';
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
@ -7,18 +6,15 @@ import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { requireAccess } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
export class TimelineService { export class TimelineService {
private accessCore: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private repository: IAssetRepository, @Inject(IAssetRepository) private repository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) { ) {}
this.accessCore = AccessCore.create(accessRepository);
}
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(auth, dto); await this.timeBucketChecks(auth, dto);
@ -60,15 +56,15 @@ export class TimelineService {
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) { if (dto.albumId) {
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
} else { } else {
dto.userId = dto.userId || auth.user.id; dto.userId = dto.userId || auth.user.id;
} }
if (dto.userId) { if (dto.userId) {
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
if (dto.isArchived !== false) { if (dto.isArchived !== false) {
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
} }
} }

View File

@ -1,6 +1,5 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AccessCore } from 'src/cores/access.core';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
@ -8,23 +7,20 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface';
import { requireAccess } from 'src/utils/access';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
export class TrashService { export class TrashService {
private access: AccessCore;
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IEventRepository) private eventRepository: IEventRepository,
) { ) {}
this.access = AccessCore.create(accessRepository);
}
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto; const { ids } = dto;
await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
await this.restoreAndSend(auth, ids); await this.restoreAndSend(auth, ids);
} }

View File

@ -1,5 +1,9 @@
import { Permission } from 'src/enum'; import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { setIsSuperset } from 'src/utils/set'; import { AuthDto } from 'src/dtos/auth.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { AlbumUserRole, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
export type GrantedRequest = { export type GrantedRequest = {
requested: Permission[]; requested: Permission[];
@ -13,3 +17,264 @@ export const isGranted = ({ requested, current }: GrantedRequest) => {
return setIsSuperset(new Set(current), new Set(requested)); return setIsSuperset(new Set(current), new Set(requested));
}; };
export type AccessRequest = {
auth: AuthDto;
permission: Permission;
ids: Set<string> | string[];
};
type SharedLinkAccessRequest = { sharedLink: SharedLinkEntity; permission: Permission; ids: Set<string> };
type OtherAccessRequest = { auth: AuthDto; permission: Permission; ids: Set<string> };
export const requireUploadAccess = (auth: AuthDto | null): AuthDto => {
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
throw new UnauthorizedException();
}
return auth;
};
export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => {
const allowedIds = await checkAccess(access, request);
if (!setIsEqual(new Set(request.ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${request.permission} access`);
}
};
export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => {
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) {
return new Set<string>();
}
return auth.sharedLink
? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet })
: checkOtherAccess(access, { auth, permission, ids: idSet });
};
const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => {
const { sharedLink, permission, ids } = request;
const sharedLinkId = sharedLink.id;
switch (permission) {
case Permission.ASSET_READ: {
return await access.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_VIEW: {
return await access.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_DOWNLOAD: {
return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
}
case Permission.ASSET_UPLOAD: {
return sharedLink.allowUpload ? ids : new Set();
}
case Permission.ASSET_SHARE: {
// TODO: fix this to not use sharedLink.userId for access control
return await access.asset.checkOwnerAccess(sharedLink.userId, ids);
}
case Permission.ALBUM_READ: {
return await access.album.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ALBUM_DOWNLOAD: {
return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
}
case Permission.ALBUM_ADD_ASSET: {
return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
}
default: {
return new Set<string>();
}
}
};
const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => {
const { auth, permission, ids } = request;
switch (permission) {
// uses album id
case Permission.ACTIVITY_CREATE: {
return await access.activity.checkCreateAccess(auth.user.id, ids);
}
// uses activity id
case Permission.ACTIVITY_DELETE: {
const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids);
const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isAlbumOwner);
}
case Permission.ASSET_READ: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_SHARE: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.ASSET_VIEW: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_DOWNLOAD: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_UPDATE: {
return await access.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_DELETE: {
return await access.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_READ: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.VIEWER,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_ADD_ASSET: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.EDITOR,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_UPDATE: {
return await access.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DELETE: {
return await access.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_SHARE: {
return await access.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DOWNLOAD: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.VIEWER,
);
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_REMOVE_ASSET: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
AlbumUserRole.EDITOR,
);
return setUnion(isOwner, isShared);
}
case Permission.ASSET_UPLOAD: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
}
case Permission.ARCHIVE_READ: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.AUTH_DEVICE_DELETE: {
return await access.authDevice.checkOwnerAccess(auth.user.id, ids);
}
case Permission.TIMELINE_READ: {
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.TIMELINE_DOWNLOAD: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.MEMORY_READ: {
return access.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_UPDATE: {
return access.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_DELETE: {
return access.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_DELETE: {
return access.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_READ: {
return await access.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_UPDATE: {
return await access.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_MERGE: {
return await access.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_CREATE: {
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_REASSIGN: {
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PARTNER_UPDATE: {
return await access.partner.checkUpdateAccess(auth.user.id, ids);
}
case Permission.STACK_READ: {
return access.stack.checkOwnerAccess(auth.user.id, ids);
}
case Permission.STACK_UPDATE: {
return access.stack.checkOwnerAccess(auth.user.id, ids);
}
case Permission.STACK_DELETE: {
return access.stack.checkOwnerAccess(auth.user.id, ids);
}
default: {
return new Set<string>();
}
}
};

View File

@ -1,10 +1,10 @@
import { AccessCore } from 'src/cores/access.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, Permission } from 'src/enum'; import { AssetFileType, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { checkAccess } from 'src/utils/access';
export interface IBulkAsset { export interface IBulkAsset {
getAssetIds: (id: string, assetIds: string[]) => Promise<Set<string>>; getAssetIds: (id: string, assetIds: string[]) => Promise<Set<string>>;
@ -23,15 +23,17 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({
export const addAssets = async ( export const addAssets = async (
auth: AuthDto, auth: AuthDto,
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, repositories: { access: IAccessRepository; bulk: IBulkAsset },
dto: { parentId: string; assetIds: string[] }, dto: { parentId: string; assetIds: string[] },
) => { ) => {
const { accessRepository, repository } = repositories; const { access, bulk } = repositories;
const access = AccessCore.create(accessRepository); const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds);
const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds);
const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id));
const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); const allowedAssetIds = await checkAccess(access, {
auth,
permission: Permission.ASSET_SHARE,
ids: notPresentAssetIds,
});
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
@ -53,7 +55,7 @@ export const addAssets = async (
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
if (newAssetIds.length > 0) { if (newAssetIds.length > 0) {
await repository.addAssetIds(dto.parentId, newAssetIds); await bulk.addAssetIds(dto.parentId, newAssetIds);
} }
return results; return results;
@ -61,18 +63,17 @@ export const addAssets = async (
export const removeAssets = async ( export const removeAssets = async (
auth: AuthDto, auth: AuthDto,
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, repositories: { access: IAccessRepository; bulk: IBulkAsset },
dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission }, dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission },
) => { ) => {
const { accessRepository, repository } = repositories; const { access, bulk } = repositories;
const access = AccessCore.create(accessRepository);
// check if the user can always remove from the parent album, memory, etc. // check if the user can always remove from the parent album, memory, etc.
const canAlwaysRemove = await access.checkAccess(auth, dto.canAlwaysRemove, [dto.parentId]); const canAlwaysRemove = await checkAccess(access, { auth, permission: dto.canAlwaysRemove, ids: [dto.parentId] });
const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds);
const allowedAssetIds = canAlwaysRemove.has(dto.parentId) const allowedAssetIds = canAlwaysRemove.has(dto.parentId)
? existingAssetIds ? existingAssetIds
: await access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); : await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds });
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
@ -94,7 +95,7 @@ export const removeAssets = async (
const removedIds = results.filter(({ success }) => success).map(({ id }) => id); const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
if (removedIds.length > 0) { if (removedIds.length > 0) {
await repository.removeAssetIds(dto.parentId, removedIds); await bulk.removeAssetIds(dto.parentId, removedIds);
} }
return results; return results;

View File

@ -1,4 +1,3 @@
import { AccessCore } from 'src/cores/access.core';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
@ -14,11 +13,7 @@ export interface IAccessRepositoryMock {
timeline: Mocked<IAccessRepository['timeline']>; timeline: Mocked<IAccessRepository['timeline']>;
} }
export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
if (reset) {
AccessCore.reset();
}
return { return {
activity: { activity: {
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),

View File

@ -6,6 +6,16 @@ export default defineConfig({
test: { test: {
root: './', root: './',
globals: true, globals: true,
coverage: {
provider: 'v8',
include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'],
thresholds: {
lines: 80,
statements: 80,
branches: 85,
functions: 85,
},
},
server: { server: {
deps: { deps: {
fallbackCJS: true, fallbackCJS: true,

View File

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { type AlbumCountResponseDto, getAlbumCount } from '@immich/sdk'; import { type AlbumStatisticsResponseDto, getAlbumStatistics } from '@immich/sdk';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let albumCountType: keyof AlbumCountResponseDto; export let albumType: keyof AlbumStatisticsResponseDto;
const handleAlbumCount = async () => { const handleAlbumCount = async () => {
try { try {
return await getAlbumCount(); return await getAlbumStatistics();
} catch { } catch {
return { owned: 0, shared: 0, notShared: 0 }; return { owned: 0, shared: 0, notShared: 0 };
} }
@ -18,6 +18,6 @@
<LoadingSpinner /> <LoadingSpinner />
{:then data} {:then data}
<div> <div>
<p>{$t('albums_count', { values: { count: data[albumCountType] } })}</p> <p>{$t('albums_count', { values: { count: data[albumType] } })}</p>
</div> </div>
{/await} {/await}

View File

@ -79,7 +79,7 @@
bind:isSelected={isSharingSelected} bind:isSelected={isSharingSelected}
> >
<svelte:fragment slot="moreInformation"> <svelte:fragment slot="moreInformation">
<MoreInformationAlbums albumCountType="shared" /> <MoreInformationAlbums albumType="shared" />
</svelte:fragment> </svelte:fragment>
</SideBarLink> </SideBarLink>
{/if} {/if}
@ -100,7 +100,7 @@
</SideBarLink> </SideBarLink>
<SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo> <SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
<svelte:fragment slot="moreInformation"> <svelte:fragment slot="moreInformation">
<MoreInformationAlbums albumCountType="owned" /> <MoreInformationAlbums albumType="owned" />
</svelte:fragment> </svelte:fragment>
</SideBarLink> </SideBarLink>