0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 02:23:09 -05:00

refactor: access repository (#15490)

This commit is contained in:
Jason Rasmussen 2025-01-21 11:09:24 -05:00 committed by GitHub
parent 318dd32363
commit b0cdd8f475
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 75 additions and 152 deletions

View file

@ -1,53 +0,0 @@
import { AlbumUserRole } from 'src/enum';
export const IAccessRepository = 'IAccessRepository';
export interface IAccessRepository {
activity: {
checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
};
asset: {
checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>>;
};
authDevice: {
checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>>;
};
album: {
checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
};
timeline: {
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
};
memory: {
checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>>;
};
person: {
checkFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
};
partner: {
checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
};
stack: {
checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>>;
};
tag: {
checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>>;
};
}

View file

@ -1,33 +1,18 @@
import { Injectable } from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { asUuid } from 'src/utils/database';
type IActivityAccess = IAccessRepository['activity'];
type IAlbumAccess = IAccessRepository['album'];
type IAssetAccess = IAccessRepository['asset'];
type IAuthDeviceAccess = IAccessRepository['authDevice'];
type IMemoryAccess = IAccessRepository['memory'];
type IPersonAccess = IAccessRepository['person'];
type IPartnerAccess = IAccessRepository['partner'];
type IStackAccess = IAccessRepository['stack'];
type ITagAccess = IAccessRepository['tag'];
type ITimelineAccess = IAccessRepository['timeline'];
@Injectable()
class ActivityAccess implements IActivityAccess {
class ActivityAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, activityIds: Set<string>) {
if (activityIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -41,9 +26,9 @@ class ActivityAccess implements IActivityAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>) {
if (activityIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -58,9 +43,9 @@ class ActivityAccess implements IActivityAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
async checkCreateAccess(userId: string, albumIds: Set<string>) {
if (albumIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -77,14 +62,14 @@ class ActivityAccess implements IActivityAccess {
}
}
class AlbumAccess implements IAlbumAccess {
class AlbumAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, albumIds: Set<string>) {
if (albumIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -99,9 +84,9 @@ class AlbumAccess implements IAlbumAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole) {
if (albumIds.size === 0) {
return new Set();
return new Set<string>();
}
const accessRole =
@ -122,9 +107,9 @@ class AlbumAccess implements IAlbumAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> {
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>) {
if (albumIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -139,14 +124,14 @@ class AlbumAccess implements IAlbumAccess {
}
}
class AssetAccess implements IAssetAccess {
class AssetAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
async checkAlbumAccess(userId: string, assetIds: Set<string>) {
if (assetIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -182,9 +167,9 @@ class AssetAccess implements IAssetAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, assetIds: Set<string>) {
if (assetIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -198,9 +183,9 @@ class AssetAccess implements IAssetAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
async checkPartnerAccess(userId: string, assetIds: Set<string>) {
if (assetIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -221,9 +206,9 @@ class AssetAccess implements IAssetAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> {
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>) {
if (assetIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -273,14 +258,14 @@ class AssetAccess implements IAssetAccess {
}
}
class AuthDeviceAccess implements IAuthDeviceAccess {
class AuthDeviceAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, deviceIds: Set<string>) {
if (deviceIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -293,14 +278,14 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
}
}
class StackAccess implements IStackAccess {
class StackAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, stackIds: Set<string>) {
if (stackIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -313,14 +298,14 @@ class StackAccess implements IStackAccess {
}
}
class TimelineAccess implements ITimelineAccess {
class TimelineAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
async checkPartnerAccess(userId: string, partnerIds: Set<string>) {
if (partnerIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -333,14 +318,14 @@ class TimelineAccess implements ITimelineAccess {
}
}
class MemoryAccess implements IMemoryAccess {
class MemoryAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, memoryIds: Set<string>) {
if (memoryIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -354,14 +339,14 @@ class MemoryAccess implements IMemoryAccess {
}
}
class PersonAccess implements IPersonAccess {
class PersonAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, personIds: Set<string>) {
if (personIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -375,9 +360,9 @@ class PersonAccess implements IPersonAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>): Promise<Set<string>> {
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>) {
if (assetFaceIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -393,14 +378,14 @@ class PersonAccess implements IPersonAccess {
}
}
class PartnerAccess implements IPartnerAccess {
class PartnerAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
async checkUpdateAccess(userId: string, partnerIds: Set<string>) {
if (partnerIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -413,14 +398,14 @@ class PartnerAccess implements IPartnerAccess {
}
}
class TagAccess implements ITagAccess {
class TagAccess {
constructor(private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>> {
async checkOwnerAccess(userId: string, tagIds: Set<string>) {
if (tagIds.size === 0) {
return new Set();
return new Set<string>();
}
return this.db
@ -433,17 +418,17 @@ class TagAccess implements ITagAccess {
}
}
export class AccessRepository implements IAccessRepository {
activity: IActivityAccess;
album: IAlbumAccess;
asset: IAssetAccess;
authDevice: IAuthDeviceAccess;
memory: IMemoryAccess;
person: IPersonAccess;
partner: IPartnerAccess;
stack: IStackAccess;
tag: ITagAccess;
timeline: ITimelineAccess;
export class AccessRepository {
activity: ActivityAccess;
album: AlbumAccess;
asset: AssetAccess;
authDevice: AuthDeviceAccess;
memory: MemoryAccess;
person: PersonAccess;
partner: PartnerAccess;
stack: StackAccess;
tag: TagAccess;
timeline: TimelineAccess;
constructor(@InjectKysely() db: Kysely<DB>) {
this.activity = new ActivityAccess(db);

View file

@ -1,4 +1,3 @@
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
@ -78,11 +77,11 @@ import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [
//
AccessRepository,
ActivityRepository,
];
export const providers = [
{ provide: IAccessRepository, useClass: AccessRepository },
{ provide: IAlbumRepository, useClass: AlbumRepository },
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository },

View file

@ -6,7 +6,6 @@ import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { Users } from 'src/db';
import { UserEntity } from 'src/entities/user.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
@ -44,6 +43,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { IViewRepository } from 'src/interfaces/view.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config';
@ -53,7 +53,7 @@ export class BaseService {
constructor(
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
protected accessRepository: AccessRepository,
protected activityRepository: ActivityRepository,
@Inject(IAuditRepository) protected auditRepository: IAuditRepository,
@Inject(IAlbumRepository) protected albumRepository: IAlbumRepository,

View file

@ -1,5 +1,6 @@
import { UserEntity } from 'src/entities/user.entity';
import { Permission } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
export type AuthApiKey = {
@ -12,6 +13,7 @@ export type AuthApiKey = {
export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
export type IActivityRepository = RepositoryInterface<ActivityRepository>;
export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
export type ActivityItem =
| Awaited<ReturnType<IActivityRepository['create']>>

View file

@ -2,7 +2,7 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { AlbumUserRole, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
export type GrantedRequest = {
@ -34,7 +34,7 @@ export const requireUploadAccess = (auth: AuthDto | null): AuthDto => {
return auth;
};
export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => {
export const requireAccess = async (access: AccessRepository, request: AccessRequest) => {
const allowedIds = await checkAccess(access, request);
if (!setIsEqual(new Set(request.ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${request.permission} access`);
@ -42,7 +42,7 @@ export const requireAccess = async (access: IAccessRepository, request: AccessRe
};
export const checkAccess = async (
access: IAccessRepository,
access: AccessRepository,
{ ids, auth, permission }: AccessRequest,
): Promise<Set<string>> => {
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
@ -56,7 +56,7 @@ export const checkAccess = async (
};
const checkSharedLinkAccess = async (
access: IAccessRepository,
access: AccessRepository,
request: SharedLinkAccessRequest,
): Promise<Set<string>> => {
const { sharedLink, permission, ids } = request;
@ -102,7 +102,7 @@ const checkSharedLinkAccess = async (
}
};
const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise<Set<string>> => {
const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRequest): Promise<Set<string>> => {
const { auth, permission, ids } = request;
switch (permission) {

View file

@ -5,12 +5,12 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, AssetType, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { AuthRequest } from 'src/middleware/auth.guard';
import { ImmichFile } from 'src/middleware/file-upload.interceptor';
import { AccessRepository } from 'src/repositories/access.repository';
import { UploadFile } from 'src/services/asset-media.service';
import { checkAccess } from 'src/utils/access';
@ -31,7 +31,7 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({
export const addAssets = async (
auth: AuthDto,
repositories: { access: IAccessRepository; bulk: IBulkAsset },
repositories: { access: AccessRepository; bulk: IBulkAsset },
dto: { parentId: string; assetIds: string[] },
) => {
const { access, bulk } = repositories;
@ -71,7 +71,7 @@ export const addAssets = async (
export const removeAssets = async (
auth: AuthDto,
repositories: { access: IAccessRepository; bulk: IBulkAsset },
repositories: { access: AccessRepository; bulk: IBulkAsset },
dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission },
) => {
const { access, bulk } = repositories;

View file

@ -1,18 +1,7 @@
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAccessRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export interface IAccessRepositoryMock {
activity: Mocked<IAccessRepository['activity']>;
asset: Mocked<IAccessRepository['asset']>;
album: Mocked<IAccessRepository['album']>;
authDevice: Mocked<IAccessRepository['authDevice']>;
memory: Mocked<IAccessRepository['memory']>;
person: Mocked<IAccessRepository['person']>;
partner: Mocked<IAccessRepository['partner']>;
stack: Mocked<IAccessRepository['stack']>;
timeline: Mocked<IAccessRepository['timeline']>;
tag: Mocked<IAccessRepository['tag']>;
}
export type IAccessRepositoryMock = { [K in keyof IAccessRepository]: Mocked<IAccessRepository[K]> };
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
return {

View file

@ -3,9 +3,10 @@ import { Writable } from 'node:stream';
import { PNG } from 'pngjs';
import { ImmichWorker } from 'src/enum';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { BaseService } from 'src/services/base.service';
import { IActivityRepository } from 'src/types';
import { IAccessRepository, IActivityRepository } from 'src/types';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock';
import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
@ -105,7 +106,7 @@ export const newTestService = <T extends BaseService>(
const sut = new Service(
loggerMock,
accessMock,
accessMock as IAccessRepository as AccessRepository,
activityMock as IActivityRepository as ActivityRepository,
auditMock,
albumMock,