mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +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:
committed by
GitHub
parent
d6887117ac
commit
cf9e04c8ec
@@ -17,6 +17,7 @@ export const databaseConfig: PostgresConnectionOptions = {
|
||||
entities: [__dirname + '/entities/*.entity.{js,ts}'],
|
||||
synchronize: false,
|
||||
migrations: [__dirname + '/migrations/*.{js,ts}'],
|
||||
subscribers: [__dirname + '/subscribers/*.{js,ts}'],
|
||||
migrationsRun: true,
|
||||
connectTimeoutMS: 10000, // 10 seconds
|
||||
...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 { AssetFaceEntity } from './asset-face.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { AuditEntity } from './audit.entity';
|
||||
import { PartnerEntity } from './partner.entity';
|
||||
import { PersonEntity } from './person.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
@@ -15,6 +16,7 @@ export * from './album.entity';
|
||||
export * from './api-key.entity';
|
||||
export * from './asset-face.entity';
|
||||
export * from './asset.entity';
|
||||
export * from './audit.entity';
|
||||
export * from './exif.entity';
|
||||
export * from './partner.entity';
|
||||
export * from './person.entity';
|
||||
@@ -30,6 +32,7 @@ export const databaseEntities = [
|
||||
APIKeyEntity,
|
||||
AssetEntity,
|
||||
AssetFaceEntity,
|
||||
AuditEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
SharedLinkEntity,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
IAccessRepository,
|
||||
IAlbumRepository,
|
||||
IAssetRepository,
|
||||
IAuditRepository,
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
IFaceRepository,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
AlbumRepository,
|
||||
APIKeyRepository,
|
||||
AssetRepository,
|
||||
AuditRepository,
|
||||
CommunicationRepository,
|
||||
CryptoRepository,
|
||||
FaceRepository,
|
||||
@@ -58,6 +60,7 @@ const providers: Provider[] = [
|
||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||
{ provide: ICommunicationRepository, useClass: CommunicationRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ 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 './api-key.repository';
|
||||
export * from './asset.repository';
|
||||
export * from './audit.repository';
|
||||
export * from './communication.repository';
|
||||
export * from './crypto.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user