mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(server): asset entity audit (#3824)
* feat(server): audit log * feedback * Insert to database * migration * test * controller/repository/service * test * module * feat(server): implement audit endpoint * directly return changed assets * add daily cleanup of audit table * fix tests * review feedback * ci * refactor(server): audit implementation * chore: open api --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
d6887117ac
commit
cf9e04c8ec
57 changed files with 899 additions and 30 deletions
215
cli/src/api/open-api/api.ts
generated
215
cli/src/api/open-api/api.ts
generated
|
@ -752,6 +752,25 @@ export const AudioCodec = {
|
||||||
export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec];
|
export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
export interface AuditDeletesResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
'ids': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
'needsFullSync': boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -1243,6 +1262,20 @@ export interface DownloadResponseDto {
|
||||||
*/
|
*/
|
||||||
'totalSize': number;
|
'totalSize': number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const EntityType = {
|
||||||
|
Asset: 'ASSET',
|
||||||
|
Album: 'ALBUM'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type EntityType = typeof EntityType[keyof typeof EntityType];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -5120,11 +5153,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
|
* @param {string} [updatedAfter]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/asset`;
|
const localVarPath = `/asset`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
@ -5166,6 +5200,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
localVarQueryParameter['skip'] = skip;
|
localVarQueryParameter['skip'] = skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updatedAfter !== undefined) {
|
||||||
|
localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ?
|
||||||
|
(updatedAfter as any).toISOString() :
|
||||||
|
updatedAfter;
|
||||||
|
}
|
||||||
|
|
||||||
if (ifNoneMatch != null) {
|
if (ifNoneMatch != null) {
|
||||||
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
||||||
}
|
}
|
||||||
|
@ -6274,12 +6314,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
|
* @param {string} [updatedAfter]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -6576,7 +6617,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
|
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get a single asset\'s information
|
* Get a single asset\'s information
|
||||||
|
@ -6895,6 +6936,13 @@ export interface AssetApiGetAllAssetsRequest {
|
||||||
*/
|
*/
|
||||||
readonly skip?: number
|
readonly skip?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetApiGetAllAssets
|
||||||
|
*/
|
||||||
|
readonly updatedAfter?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ETag of data already cached on the client
|
* ETag of data already cached on the client
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -7460,7 +7508,7 @@ export class AssetApi extends BaseAPI {
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) {
|
public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7671,6 +7719,163 @@ export class AssetApi extends BaseAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - axios parameter creator
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EntityType} entityType
|
||||||
|
* @param {string} after
|
||||||
|
* @param {string} [userId]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getAuditDeletes: async (entityType: EntityType, after: string, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'entityType' is not null or undefined
|
||||||
|
assertParamExists('getAuditDeletes', 'entityType', entityType)
|
||||||
|
// verify required parameter 'after' is not null or undefined
|
||||||
|
assertParamExists('getAuditDeletes', 'after', after)
|
||||||
|
const localVarPath = `/audit/deletes`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (entityType !== undefined) {
|
||||||
|
localVarQueryParameter['entityType'] = entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId !== undefined) {
|
||||||
|
localVarQueryParameter['userId'] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after !== undefined) {
|
||||||
|
localVarQueryParameter['after'] = (after as any instanceof Date) ?
|
||||||
|
(after as any).toISOString() :
|
||||||
|
after;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - functional programming interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiFp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EntityType} entityType
|
||||||
|
* @param {string} after
|
||||||
|
* @param {string} [userId]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getAuditDeletes(entityType: EntityType, after: string, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuditDeletesResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - factory interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = AuditApiFp(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
|
||||||
|
return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for getAuditDeletes operation in AuditApi.
|
||||||
|
* @export
|
||||||
|
* @interface AuditApiGetAuditDeletesRequest
|
||||||
|
*/
|
||||||
|
export interface AuditApiGetAuditDeletesRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {EntityType}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly entityType: EntityType
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly after: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly userId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - object-oriented interface
|
||||||
|
* @export
|
||||||
|
* @class AuditApi
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
export class AuditApi extends BaseAPI {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AuditApi
|
||||||
|
*/
|
||||||
|
public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthenticationApi - axios parameter creator
|
* AuthenticationApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
|
9
mobile/openapi/.openapi-generator/FILES
generated
9
mobile/openapi/.openapi-generator/FILES
generated
|
@ -29,6 +29,8 @@ doc/AssetResponseDto.md
|
||||||
doc/AssetStatsResponseDto.md
|
doc/AssetStatsResponseDto.md
|
||||||
doc/AssetTypeEnum.md
|
doc/AssetTypeEnum.md
|
||||||
doc/AudioCodec.md
|
doc/AudioCodec.md
|
||||||
|
doc/AuditApi.md
|
||||||
|
doc/AuditDeletesResponseDto.md
|
||||||
doc/AuthDeviceResponseDto.md
|
doc/AuthDeviceResponseDto.md
|
||||||
doc/AuthenticationApi.md
|
doc/AuthenticationApi.md
|
||||||
doc/BulkIdResponseDto.md
|
doc/BulkIdResponseDto.md
|
||||||
|
@ -50,6 +52,7 @@ doc/DeleteAssetStatus.md
|
||||||
doc/DownloadArchiveInfo.md
|
doc/DownloadArchiveInfo.md
|
||||||
doc/DownloadInfoDto.md
|
doc/DownloadInfoDto.md
|
||||||
doc/DownloadResponseDto.md
|
doc/DownloadResponseDto.md
|
||||||
|
doc/EntityType.md
|
||||||
doc/ExifResponseDto.md
|
doc/ExifResponseDto.md
|
||||||
doc/ImportAssetDto.md
|
doc/ImportAssetDto.md
|
||||||
doc/JobApi.md
|
doc/JobApi.md
|
||||||
|
@ -134,6 +137,7 @@ lib/api.dart
|
||||||
lib/api/album_api.dart
|
lib/api/album_api.dart
|
||||||
lib/api/api_key_api.dart
|
lib/api/api_key_api.dart
|
||||||
lib/api/asset_api.dart
|
lib/api/asset_api.dart
|
||||||
|
lib/api/audit_api.dart
|
||||||
lib/api/authentication_api.dart
|
lib/api/authentication_api.dart
|
||||||
lib/api/job_api.dart
|
lib/api/job_api.dart
|
||||||
lib/api/o_auth_api.dart
|
lib/api/o_auth_api.dart
|
||||||
|
@ -176,6 +180,7 @@ lib/model/asset_response_dto.dart
|
||||||
lib/model/asset_stats_response_dto.dart
|
lib/model/asset_stats_response_dto.dart
|
||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
lib/model/audio_codec.dart
|
lib/model/audio_codec.dart
|
||||||
|
lib/model/audit_deletes_response_dto.dart
|
||||||
lib/model/auth_device_response_dto.dart
|
lib/model/auth_device_response_dto.dart
|
||||||
lib/model/bulk_id_response_dto.dart
|
lib/model/bulk_id_response_dto.dart
|
||||||
lib/model/bulk_ids_dto.dart
|
lib/model/bulk_ids_dto.dart
|
||||||
|
@ -196,6 +201,7 @@ lib/model/delete_asset_status.dart
|
||||||
lib/model/download_archive_info.dart
|
lib/model/download_archive_info.dart
|
||||||
lib/model/download_info_dto.dart
|
lib/model/download_info_dto.dart
|
||||||
lib/model/download_response_dto.dart
|
lib/model/download_response_dto.dart
|
||||||
|
lib/model/entity_type.dart
|
||||||
lib/model/exif_response_dto.dart
|
lib/model/exif_response_dto.dart
|
||||||
lib/model/import_asset_dto.dart
|
lib/model/import_asset_dto.dart
|
||||||
lib/model/job_command.dart
|
lib/model/job_command.dart
|
||||||
|
@ -292,6 +298,8 @@ test/asset_response_dto_test.dart
|
||||||
test/asset_stats_response_dto_test.dart
|
test/asset_stats_response_dto_test.dart
|
||||||
test/asset_type_enum_test.dart
|
test/asset_type_enum_test.dart
|
||||||
test/audio_codec_test.dart
|
test/audio_codec_test.dart
|
||||||
|
test/audit_api_test.dart
|
||||||
|
test/audit_deletes_response_dto_test.dart
|
||||||
test/auth_device_response_dto_test.dart
|
test/auth_device_response_dto_test.dart
|
||||||
test/authentication_api_test.dart
|
test/authentication_api_test.dart
|
||||||
test/bulk_id_response_dto_test.dart
|
test/bulk_id_response_dto_test.dart
|
||||||
|
@ -313,6 +321,7 @@ test/delete_asset_status_test.dart
|
||||||
test/download_archive_info_test.dart
|
test/download_archive_info_test.dart
|
||||||
test/download_info_dto_test.dart
|
test/download_info_dto_test.dart
|
||||||
test/download_response_dto_test.dart
|
test/download_response_dto_test.dart
|
||||||
|
test/entity_type_test.dart
|
||||||
test/exif_response_dto_test.dart
|
test/exif_response_dto_test.dart
|
||||||
test/import_asset_dto_test.dart
|
test/import_asset_dto_test.dart
|
||||||
test/job_api_test.dart
|
test/job_api_test.dart
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AuditApi.md
generated
Normal file
BIN
mobile/openapi/doc/AuditApi.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AuditDeletesResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/AuditDeletesResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/EntityType.md
generated
Normal file
BIN
mobile/openapi/doc/EntityType.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/audit_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/audit_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/audit_deletes_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/audit_deletes_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/entity_type.dart
generated
Normal file
BIN
mobile/openapi/lib/model/entity_type.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/audit_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/audit_api_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/audit_deletes_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/audit_deletes_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/entity_type_test.dart
generated
Normal file
BIN
mobile/openapi/test/entity_type_test.dart
generated
Normal file
Binary file not shown.
|
@ -769,6 +769,15 @@
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "updatedAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "if-none-match",
|
"name": "if-none-match",
|
||||||
"in": "header",
|
"in": "header",
|
||||||
|
@ -2071,6 +2080,65 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/audit/deletes": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAuditDeletes",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "entityType",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/EntityType"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AuditDeletesResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Audit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/admin-sign-up": {
|
"/auth/admin-sign-up": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "adminSignUp",
|
"operationId": "adminSignUp",
|
||||||
|
@ -5239,6 +5307,24 @@
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"AuditDeletesResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"needsFullSync": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"needsFullSync",
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"AuthDeviceResponseDto": {
|
"AuthDeviceResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
|
@ -5701,6 +5787,13 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"EntityType": {
|
||||||
|
"enum": [
|
||||||
|
"ASSET",
|
||||||
|
"ALBUM"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"ExifResponseDto": {
|
"ExifResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"city": {
|
"city": {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { ICryptoRepository } from '../crypto';
|
import { ICryptoRepository } from '../crypto';
|
||||||
import { IJobRepository, JobName } from '../index';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IStorageRepository } from '../storage';
|
import { IStorageRepository } from '../storage';
|
||||||
import { AssetStats, IAssetRepository } from './asset.repository';
|
import { AssetStats, IAssetRepository } from './asset.repository';
|
||||||
import { AssetService, UploadFieldName } from './asset.service';
|
import { AssetService, UploadFieldName } from './asset.service';
|
||||||
|
|
|
@ -16,18 +16,23 @@ import {
|
||||||
AssetIdsDto,
|
AssetIdsDto,
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetJobsDto,
|
AssetJobsDto,
|
||||||
|
AssetStatsDto,
|
||||||
DownloadArchiveInfo,
|
DownloadArchiveInfo,
|
||||||
DownloadInfoDto,
|
DownloadInfoDto,
|
||||||
DownloadResponseDto,
|
DownloadResponseDto,
|
||||||
|
MapMarkerDto,
|
||||||
|
mapStats,
|
||||||
MemoryLaneDto,
|
MemoryLaneDto,
|
||||||
TimeBucketAssetDto,
|
TimeBucketAssetDto,
|
||||||
TimeBucketDto,
|
TimeBucketDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
|
import {
|
||||||
import { MapMarkerDto } from './dto/map-marker.dto';
|
AssetResponseDto,
|
||||||
import { AssetResponseDto, mapAsset, MapMarkerResponseDto } from './response-dto';
|
mapAsset,
|
||||||
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
|
MapMarkerResponseDto,
|
||||||
import { TimeBucketResponseDto } from './response-dto/time-bucket-response.dto';
|
MemoryLaneResponseDto,
|
||||||
|
TimeBucketResponseDto,
|
||||||
|
} from './response-dto';
|
||||||
|
|
||||||
export enum UploadFieldName {
|
export enum UploadFieldName {
|
||||||
ASSET_DATA = 'assetData',
|
ASSET_DATA = 'assetData',
|
||||||
|
|
|
@ -84,3 +84,8 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
|
||||||
checksum: entity.checksum.toString('base64'),
|
checksum: entity.checksum.toString('base64'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MemoryLaneResponseDto {
|
||||||
|
title!: string;
|
||||||
|
assets!: AssetResponseDto[];
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { AssetResponseDto } from './asset-response.dto';
|
|
||||||
|
|
||||||
export class MemoryLaneResponseDto {
|
|
||||||
title!: string;
|
|
||||||
assets!: AssetResponseDto[];
|
|
||||||
}
|
|
61
server/src/domain/audit/audi.service.spec.ts
Normal file
61
server/src/domain/audit/audi.service.spec.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { DatabaseAction, EntityType } from '@app/infra/entities';
|
||||||
|
import { auditStub, authStub, IAccessRepositoryMock, newAccessRepositoryMock, newAuditRepositoryMock } from '@test';
|
||||||
|
import { IAuditRepository } from './audit.repository';
|
||||||
|
import { AuditService } from './audit.service';
|
||||||
|
|
||||||
|
describe(AuditService.name, () => {
|
||||||
|
let sut: AuditService;
|
||||||
|
let accessMock: IAccessRepositoryMock;
|
||||||
|
let auditMock: jest.Mocked<IAuditRepository>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
accessMock = newAccessRepositoryMock();
|
||||||
|
auditMock = newAuditRepositoryMock();
|
||||||
|
sut = new AuditService(accessMock, auditMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleCleanup', () => {
|
||||||
|
it('should delete old audit entries', async () => {
|
||||||
|
await expect(sut.handleCleanup()).resolves.toBe(true);
|
||||||
|
expect(auditMock.removeBefore).toBeCalledWith(expect.any(Date));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDeletes', () => {
|
||||||
|
it('should require full sync if the request is older than 100 days', async () => {
|
||||||
|
auditMock.getAfter.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const date = new Date(2022, 0, 1);
|
||||||
|
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||||
|
needsFullSync: true,
|
||||||
|
ids: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||||
|
action: DatabaseAction.DELETE,
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get any new or updated assets and deleted ids', async () => {
|
||||||
|
auditMock.getAfter.mockResolvedValue([auditStub.delete]);
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||||
|
needsFullSync: false,
|
||||||
|
ids: ['asset-deleted'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||||
|
action: DatabaseAction.DELETE,
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
server/src/domain/audit/audit.dto.ts
Normal file
24
server/src/domain/audit/audit.dto.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { EntityType } from '@app/infra/entities';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsDate, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class AuditDeletesDto {
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
after!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: EntityType, enumName: 'EntityType' })
|
||||||
|
@IsEnum(EntityType)
|
||||||
|
entityType!: EntityType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID('4')
|
||||||
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuditDeletesResponseDto {
|
||||||
|
needsFullSync!: boolean;
|
||||||
|
ids!: string[];
|
||||||
|
}
|
14
server/src/domain/audit/audit.repository.ts
Normal file
14
server/src/domain/audit/audit.repository.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities';
|
||||||
|
|
||||||
|
export const IAuditRepository = 'IAuditRepository';
|
||||||
|
|
||||||
|
export interface AuditSearch {
|
||||||
|
action?: DatabaseAction;
|
||||||
|
entityType?: EntityType;
|
||||||
|
ownerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuditRepository {
|
||||||
|
getAfter(since: Date, options: AuditSearch): Promise<AuditEntity[]>;
|
||||||
|
removeBefore(before: Date): Promise<void>;
|
||||||
|
}
|
43
server/src/domain/audit/audit.service.ts
Normal file
43
server/src/domain/audit/audit.service.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { DatabaseAction } from '@app/infra/entities';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||||
|
import { AuthUserDto } from '../auth';
|
||||||
|
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
|
||||||
|
import { AuditDeletesDto, AuditDeletesResponseDto } from './audit.dto';
|
||||||
|
import { IAuditRepository } from './audit.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuditService {
|
||||||
|
private access: AccessCore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
|
@Inject(IAuditRepository) private repository: IAuditRepository,
|
||||||
|
) {
|
||||||
|
this.access = new AccessCore(accessRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCleanup(): Promise<boolean> {
|
||||||
|
await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeletes(authUser: AuthUserDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||||
|
const userId = dto.userId || authUser.id;
|
||||||
|
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, userId);
|
||||||
|
|
||||||
|
const audits = await this.repository.getAfter(dto.after, {
|
||||||
|
ownerId: userId,
|
||||||
|
entityType: dto.entityType,
|
||||||
|
action: DatabaseAction.DELETE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.after));
|
||||||
|
|
||||||
|
return {
|
||||||
|
needsFullSync: duration > AUDIT_LOG_MAX_DURATION,
|
||||||
|
ids: audits.map(({ entityId }) => entityId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
3
server/src/domain/audit/index.ts
Normal file
3
server/src/domain/audit/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './audit.dto';
|
||||||
|
export * from './audit.repository';
|
||||||
|
export * from './audit.service';
|
|
@ -1,8 +1,11 @@
|
||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
import { extname } from 'node:path';
|
import { extname } from 'node:path';
|
||||||
import pkg from 'src/../../package.json';
|
import pkg from 'src/../../package.json';
|
||||||
|
|
||||||
|
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||||
|
|
||||||
const [major, minor, patch] = pkg.version.split('.');
|
const [major, minor, patch] = pkg.version.split('.');
|
||||||
|
|
||||||
export interface IServerVersion {
|
export interface IServerVersion {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { DynamicModule, Global, Module, ModuleMetadata, OnApplicationShutdown, P
|
||||||
import { AlbumService } from './album';
|
import { AlbumService } from './album';
|
||||||
import { APIKeyService } from './api-key';
|
import { APIKeyService } from './api-key';
|
||||||
import { AssetService } from './asset';
|
import { AssetService } from './asset';
|
||||||
|
import { AuditService } from './audit';
|
||||||
import { AuthService } from './auth';
|
import { AuthService } from './auth';
|
||||||
import { FacialRecognitionService } from './facial-recognition';
|
import { FacialRecognitionService } from './facial-recognition';
|
||||||
import { JobService } from './job';
|
import { JobService } from './job';
|
||||||
|
@ -23,6 +24,7 @@ const providers: Provider[] = [
|
||||||
AlbumService,
|
AlbumService,
|
||||||
APIKeyService,
|
APIKeyService,
|
||||||
AssetService,
|
AssetService,
|
||||||
|
AuditService,
|
||||||
AuthService,
|
AuthService,
|
||||||
FacialRecognitionService,
|
FacialRecognitionService,
|
||||||
JobService,
|
JobService,
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * from './access';
|
||||||
export * from './album';
|
export * from './album';
|
||||||
export * from './api-key';
|
export * from './api-key';
|
||||||
export * from './asset';
|
export * from './asset';
|
||||||
|
export * from './audit';
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './communication';
|
export * from './communication';
|
||||||
export * from './crypto';
|
export * from './crypto';
|
||||||
|
|
|
@ -55,6 +55,7 @@ export enum JobName {
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
DELETE_FILES = 'delete-files',
|
DELETE_FILES = 'delete-files',
|
||||||
|
CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
|
||||||
|
|
||||||
// search
|
// search
|
||||||
SEARCH_INDEX_ASSETS = 'search-index-assets',
|
SEARCH_INDEX_ASSETS = 'search-index-assets',
|
||||||
|
@ -84,6 +85,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
||||||
[JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK,
|
[JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK,
|
||||||
[JobName.USER_DELETION]: QueueName.BACKGROUND_TASK,
|
[JobName.USER_DELETION]: QueueName.BACKGROUND_TASK,
|
||||||
[JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
|
[JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
|
||||||
|
[JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
|
||||||
[JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
|
[JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
|
||||||
|
|
||||||
// conversion
|
// conversion
|
||||||
|
|
|
@ -68,6 +68,9 @@ export type JobItem =
|
||||||
// Filesystem
|
// Filesystem
|
||||||
| { name: JobName.DELETE_FILES; data: IDeleteFilesJob }
|
| { name: JobName.DELETE_FILES; data: IDeleteFilesJob }
|
||||||
|
|
||||||
|
// Audit log cleanup
|
||||||
|
| { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob }
|
||||||
|
|
||||||
// Asset Deletion
|
// Asset Deletion
|
||||||
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
|
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ describe(JobService.name, () => {
|
||||||
[{ name: JobName.USER_DELETE_CHECK }],
|
[{ name: JobName.USER_DELETE_CHECK }],
|
||||||
[{ name: JobName.PERSON_CLEANUP }],
|
[{ name: JobName.PERSON_CLEANUP }],
|
||||||
[{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }],
|
[{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }],
|
||||||
|
[{ name: JobName.CLEAN_OLD_AUDIT_LOGS }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -136,6 +136,7 @@ export class JobService {
|
||||||
await this.jobRepository.queue({ name: JobName.USER_DELETE_CHECK });
|
await this.jobRepository.queue({ name: JobName.USER_DELETE_CHECK });
|
||||||
await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP });
|
await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP });
|
||||||
await this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } });
|
await this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } });
|
||||||
|
await this.jobRepository.queue({ name: JobName.CLEAN_OLD_AUDIT_LOGS });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
|
import { AssetEntity, ExifEntity } from '@app/infra/entities';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, MoreThan, Not } from 'typeorm';
|
||||||
import { In } from 'typeorm/find-options/operator/In';
|
import { In } from 'typeorm/find-options/operator/In';
|
||||||
import { Repository } from 'typeorm/repository/Repository';
|
import { Repository } from 'typeorm/repository/Repository';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
@ -131,6 +131,7 @@ export class AssetRepository implements IAssetRepository {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isFavorite: dto.isFavorite,
|
isFavorite: dto.isFavorite,
|
||||||
isArchived: dto.isArchived,
|
isArchived: dto.isArchived,
|
||||||
|
updatedAt: dto.updatedAfter ? MoreThan(dto.updatedAfter) : undefined,
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { toBoolean } from '@app/domain';
|
import { toBoolean } from '@app/domain';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
import { IsBoolean, IsDate, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class AssetSearchDto {
|
export class AssetSearchDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -32,4 +32,9 @@ export class AssetSearchDto {
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
updatedAfter?: Date;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
APIKeyController,
|
APIKeyController,
|
||||||
AppController,
|
AppController,
|
||||||
AssetController,
|
AssetController,
|
||||||
|
AuditController,
|
||||||
AuthController,
|
AuthController,
|
||||||
JobController,
|
JobController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
|
@ -42,6 +43,7 @@ import {
|
||||||
AppController,
|
AppController,
|
||||||
AlbumController,
|
AlbumController,
|
||||||
APIKeyController,
|
APIKeyController,
|
||||||
|
AuditController,
|
||||||
AuthController,
|
AuthController,
|
||||||
JobController,
|
JobController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
|
|
|
@ -9,14 +9,14 @@ import {
|
||||||
AuthUserDto,
|
AuthUserDto,
|
||||||
DownloadInfoDto,
|
DownloadInfoDto,
|
||||||
DownloadResponseDto,
|
DownloadResponseDto,
|
||||||
|
MapMarkerDto,
|
||||||
MapMarkerResponseDto,
|
MapMarkerResponseDto,
|
||||||
MemoryLaneDto,
|
MemoryLaneDto,
|
||||||
|
MemoryLaneResponseDto,
|
||||||
TimeBucketAssetDto,
|
TimeBucketAssetDto,
|
||||||
TimeBucketDto,
|
TimeBucketDto,
|
||||||
TimeBucketResponseDto,
|
TimeBucketResponseDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
|
|
||||||
import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
|
|
||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
|
||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
||||||
|
|
18
server/src/immich/controllers/audit.controller.ts
Normal file
18
server/src/immich/controllers/audit.controller.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { AuditDeletesDto, AuditDeletesResponseDto, AuditService, AuthUserDto } from '@app/domain';
|
||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
|
import { UseValidation } from '../app.utils';
|
||||||
|
|
||||||
|
@ApiTags('Audit')
|
||||||
|
@Controller('audit')
|
||||||
|
@Authenticated()
|
||||||
|
@UseValidation()
|
||||||
|
export class AuditController {
|
||||||
|
constructor(private service: AuditService) {}
|
||||||
|
|
||||||
|
@Get('deletes')
|
||||||
|
getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||||
|
return this.service.getDeletes(authUser, dto);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ export * from './album.controller';
|
||||||
export * from './api-key.controller';
|
export * from './api-key.controller';
|
||||||
export * from './app.controller';
|
export * from './app.controller';
|
||||||
export * from './asset.controller';
|
export * from './asset.controller';
|
||||||
|
export * from './audit.controller';
|
||||||
export * from './auth.controller';
|
export * from './auth.controller';
|
||||||
export * from './job.controller';
|
export * from './job.controller';
|
||||||
export * from './oauth.controller';
|
export * from './oauth.controller';
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const databaseConfig: PostgresConnectionOptions = {
|
||||||
entities: [__dirname + '/entities/*.entity.{js,ts}'],
|
entities: [__dirname + '/entities/*.entity.{js,ts}'],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
migrations: [__dirname + '/migrations/*.{js,ts}'],
|
migrations: [__dirname + '/migrations/*.{js,ts}'],
|
||||||
|
subscribers: [__dirname + '/subscribers/*.{js,ts}'],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
connectTimeoutMS: 10000, // 10 seconds
|
connectTimeoutMS: 10000, // 10 seconds
|
||||||
...urlOrParts,
|
...urlOrParts,
|
||||||
|
|
34
server/src/infra/entities/audit.entity.ts
Normal file
34
server/src/infra/entities/audit.entity.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export enum DatabaseAction {
|
||||||
|
CREATE = 'CREATE',
|
||||||
|
UPDATE = 'UPDATE',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EntityType {
|
||||||
|
ASSET = 'ASSET',
|
||||||
|
ALBUM = 'ALBUM',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('audit')
|
||||||
|
@Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt'])
|
||||||
|
export class AuditEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
entityType!: EntityType;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
entityId!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
action!: DatabaseAction;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
ownerId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { AlbumEntity } from './album.entity';
|
||||||
import { APIKeyEntity } from './api-key.entity';
|
import { APIKeyEntity } from './api-key.entity';
|
||||||
import { AssetFaceEntity } from './asset-face.entity';
|
import { AssetFaceEntity } from './asset-face.entity';
|
||||||
import { AssetEntity } from './asset.entity';
|
import { AssetEntity } from './asset.entity';
|
||||||
|
import { AuditEntity } from './audit.entity';
|
||||||
import { PartnerEntity } from './partner.entity';
|
import { PartnerEntity } from './partner.entity';
|
||||||
import { PersonEntity } from './person.entity';
|
import { PersonEntity } from './person.entity';
|
||||||
import { SharedLinkEntity } from './shared-link.entity';
|
import { SharedLinkEntity } from './shared-link.entity';
|
||||||
|
@ -15,6 +16,7 @@ export * from './album.entity';
|
||||||
export * from './api-key.entity';
|
export * from './api-key.entity';
|
||||||
export * from './asset-face.entity';
|
export * from './asset-face.entity';
|
||||||
export * from './asset.entity';
|
export * from './asset.entity';
|
||||||
|
export * from './audit.entity';
|
||||||
export * from './exif.entity';
|
export * from './exif.entity';
|
||||||
export * from './partner.entity';
|
export * from './partner.entity';
|
||||||
export * from './person.entity';
|
export * from './person.entity';
|
||||||
|
@ -30,6 +32,7 @@ export const databaseEntities = [
|
||||||
APIKeyEntity,
|
APIKeyEntity,
|
||||||
AssetEntity,
|
AssetEntity,
|
||||||
AssetFaceEntity,
|
AssetFaceEntity,
|
||||||
|
AuditEntity,
|
||||||
PartnerEntity,
|
PartnerEntity,
|
||||||
PersonEntity,
|
PersonEntity,
|
||||||
SharedLinkEntity,
|
SharedLinkEntity,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
IAccessRepository,
|
IAccessRepository,
|
||||||
IAlbumRepository,
|
IAlbumRepository,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
|
IAuditRepository,
|
||||||
ICommunicationRepository,
|
ICommunicationRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
IFaceRepository,
|
IFaceRepository,
|
||||||
|
@ -35,6 +36,7 @@ import {
|
||||||
AlbumRepository,
|
AlbumRepository,
|
||||||
APIKeyRepository,
|
APIKeyRepository,
|
||||||
AssetRepository,
|
AssetRepository,
|
||||||
|
AuditRepository,
|
||||||
CommunicationRepository,
|
CommunicationRepository,
|
||||||
CryptoRepository,
|
CryptoRepository,
|
||||||
FaceRepository,
|
FaceRepository,
|
||||||
|
@ -58,6 +60,7 @@ const providers: Provider[] = [
|
||||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
|
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||||
{ provide: ICommunicationRepository, useClass: CommunicationRepository },
|
{ provide: ICommunicationRepository, useClass: CommunicationRepository },
|
||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
{ provide: IFaceRepository, useClass: FaceRepository },
|
{ provide: IFaceRepository, useClass: FaceRepository },
|
||||||
|
|
16
server/src/infra/migrations/1692804658140-AddAuditTable.ts
Normal file
16
server/src/infra/migrations/1692804658140-AddAuditTable.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddAuditTable1692804658140 implements MigrationInterface {
|
||||||
|
name = 'AddAuditTable1692804658140'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "audit" ("id" SERIAL NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_ownerId_createdAt" ON "audit" ("ownerId", "createdAt") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_ownerId_createdAt"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "audit"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
26
server/src/infra/repositories/audit.repository.ts
Normal file
26
server/src/infra/repositories/audit.repository.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { AuditSearch, IAuditRepository } from '@app/domain';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { LessThan, MoreThan, Repository } from 'typeorm';
|
||||||
|
import { AuditEntity } from '../entities';
|
||||||
|
|
||||||
|
export class AuditRepository implements IAuditRepository {
|
||||||
|
constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {}
|
||||||
|
|
||||||
|
getAfter(since: Date, options: AuditSearch): Promise<AuditEntity[]> {
|
||||||
|
return this.repository
|
||||||
|
.createQueryBuilder('audit')
|
||||||
|
.where({
|
||||||
|
createdAt: MoreThan(since),
|
||||||
|
action: options.action,
|
||||||
|
entityType: options.entityType,
|
||||||
|
ownerId: options.ownerId,
|
||||||
|
})
|
||||||
|
.distinctOn(['audit.entityId', 'audit.entityType'])
|
||||||
|
.orderBy('audit.entityId, audit.entityType, audit.createdAt', 'DESC')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeBefore(before: Date): Promise<void> {
|
||||||
|
await this.repository.delete({ createdAt: LessThan(before) });
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ export * from './access.repository';
|
||||||
export * from './album.repository';
|
export * from './album.repository';
|
||||||
export * from './api-key.repository';
|
export * from './api-key.repository';
|
||||||
export * from './asset.repository';
|
export * from './asset.repository';
|
||||||
|
export * from './audit.repository';
|
||||||
export * from './communication.repository';
|
export * from './communication.repository';
|
||||||
export * from './crypto.repository';
|
export * from './crypto.repository';
|
||||||
export * from './face.repository';
|
export * from './face.repository';
|
||||||
|
|
38
server/src/infra/subscribers/audit.subscriber.ts
Normal file
38
server/src/infra/subscribers/audit.subscriber.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm';
|
||||||
|
import { AlbumEntity, AssetEntity, AuditEntity, DatabaseAction, EntityType } from '../entities';
|
||||||
|
|
||||||
|
@EventSubscriber()
|
||||||
|
export class AuditSubscriber implements EntitySubscriberInterface<AssetEntity | AlbumEntity> {
|
||||||
|
async afterRemove(event: RemoveEvent<AssetEntity>): Promise<void> {
|
||||||
|
await this.onEvent(DatabaseAction.DELETE, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onEvent<T>(action: DatabaseAction, event: RemoveEvent<T>): Promise<any> {
|
||||||
|
const audit = this.getAudit(event.metadata.name, { ...event.entity, id: event.entityId });
|
||||||
|
if (audit && audit.entityId && audit.ownerId) {
|
||||||
|
await event.manager.getRepository(AuditEntity).save({ ...audit, action });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAudit(entityName: string, entity: any): Partial<AuditEntity> | null {
|
||||||
|
switch (entityName) {
|
||||||
|
case AssetEntity.name:
|
||||||
|
const asset = entity as AssetEntity;
|
||||||
|
return {
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
entityId: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
case AlbumEntity.name:
|
||||||
|
const album = entity as AlbumEntity;
|
||||||
|
return {
|
||||||
|
entityType: EntityType.ALBUM,
|
||||||
|
entityId: album.id,
|
||||||
|
ownerId: album.ownerId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AuditService,
|
||||||
FacialRecognitionService,
|
FacialRecognitionService,
|
||||||
IDeleteFilesJob,
|
IDeleteFilesJob,
|
||||||
JobName,
|
JobName,
|
||||||
|
@ -35,11 +36,13 @@ export class AppService {
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private systemConfigService: SystemConfigService,
|
private systemConfigService: SystemConfigService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private auditService: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.jobService.registerHandlers({
|
await this.jobService.registerHandlers({
|
||||||
[JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
|
[JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
|
||||||
|
[JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
|
||||||
[JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
|
[JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
|
||||||
[JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
|
[JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
|
||||||
[JobName.QUEUE_OBJECT_TAGGING]: (data) => this.smartInfoService.handleQueueObjectTagging(data),
|
[JobName.QUEUE_OBJECT_TAGGING]: (data) => this.smartInfoService.handleQueueObjectTagging(data),
|
||||||
|
|
|
@ -408,7 +408,11 @@ export class MetadataExtractionProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
|
await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
|
||||||
await this.assetRepository.save({ id: asset.id, fileCreatedAt: fileCreatedAt || undefined });
|
await this.assetRepository.save({
|
||||||
|
id: asset.id,
|
||||||
|
fileCreatedAt: fileCreatedAt || undefined,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
29
server/test/fixtures/audit.stub.ts
vendored
Normal file
29
server/test/fixtures/audit.stub.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities';
|
||||||
|
import { authStub } from './auth.stub';
|
||||||
|
|
||||||
|
export const auditStub = {
|
||||||
|
create: Object.freeze<AuditEntity>({
|
||||||
|
id: 1,
|
||||||
|
entityId: 'asset-created',
|
||||||
|
action: DatabaseAction.CREATE,
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
}),
|
||||||
|
update: Object.freeze<AuditEntity>({
|
||||||
|
id: 2,
|
||||||
|
entityId: 'asset-updated',
|
||||||
|
action: DatabaseAction.UPDATE,
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
}),
|
||||||
|
delete: Object.freeze<AuditEntity>({
|
||||||
|
id: 3,
|
||||||
|
entityId: 'asset-deleted',
|
||||||
|
action: DatabaseAction.DELETE,
|
||||||
|
entityType: EntityType.ASSET,
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
}),
|
||||||
|
};
|
1
server/test/fixtures/index.ts
vendored
1
server/test/fixtures/index.ts
vendored
|
@ -1,6 +1,7 @@
|
||||||
export * from './album.stub';
|
export * from './album.stub';
|
||||||
export * from './api-key.stub';
|
export * from './api-key.stub';
|
||||||
export * from './asset.stub';
|
export * from './asset.stub';
|
||||||
|
export * from './audit.stub';
|
||||||
export * from './auth.stub';
|
export * from './auth.stub';
|
||||||
export * from './device.stub';
|
export * from './device.stub';
|
||||||
export * from './error.stub';
|
export * from './error.stub';
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { IAccessRepository } from '@app/domain';
|
import { IAccessRepository } from '@app/domain';
|
||||||
|
|
||||||
export type IAccessRepositoryMock = {
|
export interface IAccessRepositoryMock {
|
||||||
asset: jest.Mocked<IAccessRepository['asset']>;
|
asset: jest.Mocked<IAccessRepository['asset']>;
|
||||||
album: jest.Mocked<IAccessRepository['album']>;
|
album: jest.Mocked<IAccessRepository['album']>;
|
||||||
library: jest.Mocked<IAccessRepository['library']>;
|
library: jest.Mocked<IAccessRepository['library']>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||||
return {
|
return {
|
||||||
|
|
8
server/test/repositories/audit.repository.mock.ts
Normal file
8
server/test/repositories/audit.repository.mock.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { IAuditRepository } from '@app/domain';
|
||||||
|
|
||||||
|
export const newAuditRepositoryMock = (): jest.Mocked<IAuditRepository> => {
|
||||||
|
return {
|
||||||
|
getAfter: jest.fn(),
|
||||||
|
removeBefore: jest.fn(),
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,6 +2,7 @@ export * from './access.repository.mock';
|
||||||
export * from './album.repository.mock';
|
export * from './album.repository.mock';
|
||||||
export * from './api-key.repository.mock';
|
export * from './api-key.repository.mock';
|
||||||
export * from './asset.repository.mock';
|
export * from './asset.repository.mock';
|
||||||
|
export * from './audit.repository.mock';
|
||||||
export * from './communication.repository.mock';
|
export * from './communication.repository.mock';
|
||||||
export * from './crypto.repository.mock';
|
export * from './crypto.repository.mock';
|
||||||
export * from './face.repository.mock';
|
export * from './face.repository.mock';
|
||||||
|
|
215
web/src/api/open-api/api.ts
generated
215
web/src/api/open-api/api.ts
generated
|
@ -752,6 +752,25 @@ export const AudioCodec = {
|
||||||
export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec];
|
export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
export interface AuditDeletesResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
'ids': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AuditDeletesResponseDto
|
||||||
|
*/
|
||||||
|
'needsFullSync': boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -1243,6 +1262,20 @@ export interface DownloadResponseDto {
|
||||||
*/
|
*/
|
||||||
'totalSize': number;
|
'totalSize': number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const EntityType = {
|
||||||
|
Asset: 'ASSET',
|
||||||
|
Album: 'ALBUM'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type EntityType = typeof EntityType[keyof typeof EntityType];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -5120,11 +5153,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
|
* @param {string} [updatedAfter]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/asset`;
|
const localVarPath = `/asset`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
@ -5166,6 +5200,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
localVarQueryParameter['skip'] = skip;
|
localVarQueryParameter['skip'] = skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updatedAfter !== undefined) {
|
||||||
|
localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ?
|
||||||
|
(updatedAfter as any).toISOString() :
|
||||||
|
updatedAfter;
|
||||||
|
}
|
||||||
|
|
||||||
if (ifNoneMatch != null) {
|
if (ifNoneMatch != null) {
|
||||||
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
||||||
}
|
}
|
||||||
|
@ -6274,12 +6314,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
* @param {boolean} [withoutThumbs] Include assets without thumbnails
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
|
* @param {string} [updatedAfter]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -6576,7 +6617,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
|
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get a single asset\'s information
|
* Get a single asset\'s information
|
||||||
|
@ -6895,6 +6936,13 @@ export interface AssetApiGetAllAssetsRequest {
|
||||||
*/
|
*/
|
||||||
readonly skip?: number
|
readonly skip?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetApiGetAllAssets
|
||||||
|
*/
|
||||||
|
readonly updatedAfter?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ETag of data already cached on the client
|
* ETag of data already cached on the client
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -7460,7 +7508,7 @@ export class AssetApi extends BaseAPI {
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) {
|
public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7671,6 +7719,163 @@ export class AssetApi extends BaseAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - axios parameter creator
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EntityType} entityType
|
||||||
|
* @param {string} after
|
||||||
|
* @param {string} [userId]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getAuditDeletes: async (entityType: EntityType, after: string, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'entityType' is not null or undefined
|
||||||
|
assertParamExists('getAuditDeletes', 'entityType', entityType)
|
||||||
|
// verify required parameter 'after' is not null or undefined
|
||||||
|
assertParamExists('getAuditDeletes', 'after', after)
|
||||||
|
const localVarPath = `/audit/deletes`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (entityType !== undefined) {
|
||||||
|
localVarQueryParameter['entityType'] = entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId !== undefined) {
|
||||||
|
localVarQueryParameter['userId'] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after !== undefined) {
|
||||||
|
localVarQueryParameter['after'] = (after as any instanceof Date) ?
|
||||||
|
(after as any).toISOString() :
|
||||||
|
after;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - functional programming interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiFp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EntityType} entityType
|
||||||
|
* @param {string} after
|
||||||
|
* @param {string} [userId]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getAuditDeletes(entityType: EntityType, after: string, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuditDeletesResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - factory interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = AuditApiFp(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
|
||||||
|
return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for getAuditDeletes operation in AuditApi.
|
||||||
|
* @export
|
||||||
|
* @interface AuditApiGetAuditDeletesRequest
|
||||||
|
*/
|
||||||
|
export interface AuditApiGetAuditDeletesRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {EntityType}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly entityType: EntityType
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly after: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AuditApiGetAuditDeletes
|
||||||
|
*/
|
||||||
|
readonly userId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuditApi - object-oriented interface
|
||||||
|
* @export
|
||||||
|
* @class AuditApi
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
export class AuditApi extends BaseAPI {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AuditApi
|
||||||
|
*/
|
||||||
|
public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthenticationApi - axios parameter creator
|
* AuthenticationApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
|
Loading…
Reference in a new issue