Compare commits

...

9 commits

Author SHA1 Message Date
Alex Tran
023b64ad02 module 2023-08-08 14:22:58 -05:00
Alex Tran
93686edaa3 test 2023-08-08 13:21:06 -05:00
Alex Tran
17902861a9 controller/repository/service 2023-08-08 13:14:01 -05:00
Alex Tran
cee2c2ec45 test 2023-08-08 12:54:24 -05:00
Alex Tran
69800f78dc migration 2023-08-08 12:27:09 -05:00
Alex Tran
f2bb21ea5d Merge branch 'main' of github.com:immich-app/immich into dev/audit-log 2023-08-08 10:03:53 -05:00
Alex Tran
707b9c68a0 Insert to database 2023-08-08 05:55:50 -05:00
Alex Tran
3aea58a015 feedback 2023-08-08 05:27:50 -05:00
Alex Tran
4e760c50a7 feat(server): audit log 2023-08-07 21:40:50 -05:00
17 changed files with 196 additions and 0 deletions

View file

@ -2021,6 +2021,38 @@
]
}
},
"/audit": {
"get": {
"operationId": "getAuditRecords",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Audit"
]
}
},
"/auth/admin-sign-up": {
"post": {
"operationId": "adminSignUp",

View file

@ -0,0 +1,7 @@
import { AuditEntity } from '@app/infra/entities';
export const IAuditRepository = 'IAuditRepository';
export interface IAuditRepository {
get(): Promise<AuditEntity[]>;
}

View file

@ -0,0 +1,13 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { IAuditRepository } from '.';
@Injectable()
export class AuditService {
private logger = new Logger(AuditService.name);
constructor(@Inject(IAuditRepository) private repository: IAuditRepository) {}
async getAuditRecords(): Promise<any> {
return this.repository.get();
}
}

View file

@ -0,0 +1,2 @@
export * from './audit.repository';
export * from './audit.service';

View file

@ -2,6 +2,7 @@ import { DynamicModule, Global, Module, ModuleMetadata, OnApplicationShutdown, P
import { AlbumService } from './album';
import { APIKeyService } from './api-key';
import { AssetService } from './asset';
import { AuditService } from './audit';
import { AuthService } from './auth';
import { FacialRecognitionService } from './facial-recognition';
import { JobService } from './job';
@ -46,6 +47,7 @@ const providers: Provider[] = [
return configService.getConfig();
},
},
AuditService,
];
@Global()

View file

@ -2,6 +2,7 @@ export * from './access';
export * from './album';
export * from './api-key';
export * from './asset';
export * from './audit';
export * from './auth';
export * from './communication';
export * from './crypto';

View file

@ -16,6 +16,7 @@ import {
APIKeyController,
AppController,
AssetController,
AuditController,
AuthController,
JobController,
OAuthController,
@ -42,6 +43,7 @@ import {
AppController,
AlbumController,
APIKeyController,
AuditController,
AuthController,
JobController,
OAuthController,

View file

@ -0,0 +1,18 @@
import { AuditService } from '@app/domain/audit';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
@ApiTags('Audit')
@Controller('audit')
@Authenticated({ admin: true })
@UseValidation()
export class AuditController {
constructor(private service: AuditService) {}
@Get()
getAuditRecords(): Promise<any> {
return this.service.getAuditRecords();
}
}

View file

@ -2,6 +2,7 @@ export * from './album.controller';
export * from './api-key.controller';
export * from './app.controller';
export * from './asset.controller';
export * from './audit.controller';
export * from './auth.controller';
export * from './job.controller';
export * from './oauth.controller';

View file

@ -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,

View file

@ -0,0 +1,35 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
export enum DatabaseAction {
CREATE = 'CREATE',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
}
export enum EntityType {
ASSET = 'ASSET',
}
@Entity('audit')
export class AuditEntity {
@PrimaryGeneratedColumn('increment')
id!: number;
@Column()
entityType!: EntityType;
@Column()
entityId!: string;
@Column()
action!: DatabaseAction;
@Column()
ownerId!: string;
@Column()
userId!: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date;
}

View file

@ -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,

View file

@ -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 },

View file

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAuditTable1691462205943 implements MigrationInterface {
name = 'AddAuditTable1691462205943';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "audit" ("id" SERIAL NOT NULL, "entityType" character varying NOT NULL, "entityId" character varying NOT NULL, "action" character varying NOT NULL, "ownerId" character varying NOT NULL, "userId" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id"))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "audit"`);
}
}

View file

@ -0,0 +1,12 @@
import { IAuditRepository } from '@app/domain/audit/audit.repository';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuditEntity } from '../entities';
export class AuditRepository implements IAuditRepository {
constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {}
get(): Promise<AuditEntity[]> {
return this.repository.find();
}
}

View file

@ -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';

View file

@ -0,0 +1,48 @@
import { EntitySubscriberInterface, EventSubscriber, InsertEvent, LoadEvent, RemoveEvent, UpdateEvent } from 'typeorm';
import { AssetEntity, AuditEntity, DatabaseAction, EntityType } from '../entities';
@EventSubscriber()
export class AssetAudit implements EntitySubscriberInterface<AssetEntity> {
private assetEntity: AssetEntity = new AssetEntity();
listenTo() {
return AssetEntity;
}
afterLoad(entity: AssetEntity): void | Promise<any> {
this.assetEntity = entity;
}
async afterInsert(event: InsertEvent<AssetEntity>): Promise<any> {
const auditRepository = event.manager.getRepository(AuditEntity);
const auditEntity = this.getAssetAudit(DatabaseAction.CREATE);
await auditRepository.save(auditEntity);
}
async afterRemove(event: RemoveEvent<AssetEntity>): Promise<any> {
const auditRepository = event.manager.getRepository(AuditEntity);
const auditEntity = this.getAssetAudit(DatabaseAction.DELETE);
await auditRepository.save(auditEntity);
}
async afterUpdate(event: UpdateEvent<AssetEntity>): Promise<any> {
const auditRepository = event.manager.getRepository(AuditEntity);
const auditEntity = this.getAssetAudit(DatabaseAction.UPDATE);
await auditRepository.save(auditEntity);
}
private getAssetAudit(action: DatabaseAction): AuditEntity {
const auditEntity = new AuditEntity();
auditEntity.action = action;
auditEntity.entityType = EntityType.ASSET;
auditEntity.entityId = this.assetEntity.id;
auditEntity.ownerId = this.assetEntity.ownerId;
auditEntity.userId = this.assetEntity.ownerId;
return auditEntity;
}
}