mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
refactor: access core (#11930)
This commit is contained in:
parent
c7801eae7e
commit
8285803c95
@ -1,312 +0,0 @@
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
|
||||
|
||||
let instance: AccessCore | null;
|
||||
|
||||
export class AccessCore {
|
||||
private constructor(private repository: IAccessRepository) {}
|
||||
|
||||
static create(repository: IAccessRepository) {
|
||||
if (!instance) {
|
||||
instance = new AccessCore(repository);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
static reset() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
requireUploadAccess(auth: AuthDto | null): AuthDto {
|
||||
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to all ids, for the given permission.
|
||||
* Throws error if user does not have access to any of the ids.
|
||||
*/
|
||||
async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) {
|
||||
ids = Array.isArray(ids) ? ids : [ids];
|
||||
const allowedIds = await this.checkAccess(auth, permission, ids);
|
||||
if (!setIsEqual(new Set(ids), allowedIds)) {
|
||||
throw new BadRequestException(`Not found or no ${permission} access`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return ids that user has access to, for the given permission.
|
||||
* Check is done for each id, and only allowed ids are returned.
|
||||
*
|
||||
* @returns Set<string>
|
||||
*/
|
||||
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]): Promise<Set<string>> {
|
||||
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
||||
if (idSet.size === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
if (auth.sharedLink) {
|
||||
return this.checkAccessSharedLink(auth.sharedLink, permission, idSet);
|
||||
}
|
||||
|
||||
return this.checkAccessOther(auth, permission, idSet);
|
||||
}
|
||||
|
||||
private async checkAccessSharedLink(
|
||||
sharedLink: SharedLinkEntity,
|
||||
permission: Permission,
|
||||
ids: Set<string>,
|
||||
): Promise<Set<string>> {
|
||||
const sharedLinkId = sharedLink.id;
|
||||
|
||||
switch (permission) {
|
||||
case Permission.ASSET_READ: {
|
||||
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
return sharedLink.allowDownload
|
||||
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return sharedLink.allowUpload ? ids : new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
// TODO: fix this to not use sharedLink.userId for access control
|
||||
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
return sharedLink.allowDownload
|
||||
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
case Permission.ALBUM_ADD_ASSET: {
|
||||
return sharedLink.allowUpload
|
||||
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>): Promise<Set<string>> {
|
||||
switch (permission) {
|
||||
// uses album id
|
||||
case Permission.ACTIVITY_CREATE: {
|
||||
return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
// uses activity id
|
||||
case Permission.ACTIVITY_DELETE: {
|
||||
const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
);
|
||||
return setUnion(isOwner, isAlbumOwner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_READ: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPDATE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DELETE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_RESTORE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await this.repository.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.VIEWER,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_ADD_ASSET: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await this.repository.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.EDITOR,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_UPDATE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DELETE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_SHARE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await this.repository.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.VIEWER,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_REMOVE_ASSET: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await this.repository.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.EDITOR,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
}
|
||||
|
||||
case Permission.ARCHIVE_READ: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.AUTH_DEVICE_DELETE: {
|
||||
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_READ: {
|
||||
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_DOWNLOAD: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.MEMORY_READ: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_UPDATE: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_DELETE: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_DELETE: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_UPDATE: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_CREATE: {
|
||||
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_REASSIGN: {
|
||||
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PARTNER_UPDATE: {
|
||||
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_READ: {
|
||||
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_UPDATE: {
|
||||
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_DELETE: {
|
||||
return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import {
|
||||
ActivityCreateDto,
|
||||
ActivityDto,
|
||||
@ -16,20 +15,17 @@ import { ActivityEntity } from 'src/entities/activity.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class ActivityService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IActivityRepository) private repository: IActivityRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
const activities = await this.repository.search({
|
||||
userId: dto.userId,
|
||||
albumId: dto.albumId,
|
||||
@ -41,12 +37,12 @@ export class ActivityService {
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
|
||||
await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
|
||||
|
||||
const common = {
|
||||
userId: auth.user.id,
|
||||
@ -80,7 +76,7 @@ export class ActivityService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import {
|
||||
AddUsersDto,
|
||||
AlbumCountResponseDto,
|
||||
@ -24,21 +23,19 @@ import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfa
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
private access: AccessCore;
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async getCount(auth: AuthDto): Promise<AlbumCountResponseDto> {
|
||||
const [owned, shared, notShared] = await Promise.all([
|
||||
@ -102,7 +99,7 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] });
|
||||
await this.albumRepository.updateThumbnails();
|
||||
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
|
||||
const album = await this.findOrFail(id, { withAssets });
|
||||
@ -126,7 +123,11 @@ export class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds));
|
||||
const allowedAssetIdsSet = await checkAccess(this.access, {
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: dto.assetIds || [],
|
||||
});
|
||||
const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
|
||||
|
||||
const album = await this.albumRepository.create({
|
||||
@ -146,7 +147,7 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
|
||||
@ -169,17 +170,17 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] });
|
||||
await this.albumRepository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
|
||||
|
||||
const results = await addAssets(
|
||||
auth,
|
||||
{ accessRepository: this.accessRepository, repository: this.albumRepository },
|
||||
{ access: this.access, bulk: this.albumRepository },
|
||||
{ parentId: id, assetIds: dto.ids },
|
||||
);
|
||||
|
||||
@ -198,12 +199,12 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const results = await removeAssets(
|
||||
auth,
|
||||
{ accessRepository: this.accessRepository, repository: this.albumRepository },
|
||||
{ access: this.access, bulk: this.albumRepository },
|
||||
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE },
|
||||
);
|
||||
|
||||
@ -219,7 +220,7 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
@ -263,15 +264,14 @@ export class AlbumService {
|
||||
|
||||
// non-admin can remove themselves
|
||||
if (auth.user.id !== userId) {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
}
|
||||
|
||||
await this.albumUserRepository.delete({ albumId: id, userId });
|
||||
}
|
||||
|
||||
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
|
||||
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { extname } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
@ -36,6 +35,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
@ -57,10 +57,8 @@ export interface UploadFile {
|
||||
|
||||
@Injectable()
|
||||
export class AssetMediaService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@ -69,7 +67,6 @@ export class AssetMediaService {
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(AssetMediaService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
|
||||
async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> {
|
||||
@ -86,7 +83,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
canUploadFile({ auth, fieldName, file }: UploadRequest): true {
|
||||
this.access.requireUploadAccess(auth);
|
||||
requireUploadAccess(auth);
|
||||
|
||||
const filename = file.originalName;
|
||||
|
||||
@ -118,7 +115,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
|
||||
this.access.requireUploadAccess(auth);
|
||||
requireUploadAccess(auth);
|
||||
|
||||
const originalExtension = extname(file.originalName);
|
||||
|
||||
@ -132,7 +129,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
getUploadFolder({ auth, fieldName, file }: UploadRequest): string {
|
||||
auth = this.access.requireUploadAccess(auth);
|
||||
auth = requireUploadAccess(auth);
|
||||
|
||||
let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid);
|
||||
if (fieldName === UploadFieldName.PROFILE_DATA) {
|
||||
@ -151,12 +148,12 @@ export class AssetMediaService {
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
try {
|
||||
await this.access.requirePermission(
|
||||
await requireAccess(this.access, {
|
||||
auth,
|
||||
Permission.ASSET_UPLOAD,
|
||||
permission: Permission.ASSET_UPLOAD,
|
||||
// do not need an id here, but the interface requires it
|
||||
auth.user.id,
|
||||
);
|
||||
ids: [auth.user.id],
|
||||
});
|
||||
|
||||
this.requireQuota(auth, file.size);
|
||||
|
||||
@ -195,7 +192,7 @@ export class AssetMediaService {
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
try {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
const asset = (await this.assetRepository.getById(id)) as AssetEntity;
|
||||
|
||||
this.requireQuota(auth, file.size);
|
||||
@ -219,7 +216,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
if (!asset) {
|
||||
@ -234,7 +231,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_VIEW, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
||||
@ -257,7 +254,7 @@ export class AssetMediaService {
|
||||
}
|
||||
|
||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_VIEW, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
if (!asset) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import {
|
||||
AssetResponseDto,
|
||||
@ -39,15 +38,15 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
export class AssetService {
|
||||
private access: AccessCore;
|
||||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@ -58,7 +57,6 @@ export class AssetService {
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(AssetService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
@ -109,7 +107,7 @@ export class AssetService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] });
|
||||
|
||||
const asset = await this.assetRepository.getById(
|
||||
id,
|
||||
@ -158,7 +156,7 @@ export class AssetService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||
@ -182,7 +180,7 @@ export class AssetService {
|
||||
|
||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids });
|
||||
|
||||
for (const id of ids) {
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||
@ -278,7 +276,7 @@ export class AssetService {
|
||||
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
||||
const { ids, force } = dto;
|
||||
|
||||
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
|
||||
|
||||
if (force) {
|
||||
await this.jobRepository.queueAll(
|
||||
@ -294,7 +292,7 @@ export class AssetService {
|
||||
}
|
||||
|
||||
async run(auth: AuthDto, dto: AssetJobsDto) {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
|
||||
const jobs: JobItem[] = [];
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { resolve } from 'node:path';
|
||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import {
|
||||
AuditDeletesDto,
|
||||
@ -24,15 +23,14 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||
@ -41,7 +39,6 @@ export class AuditService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.logger.setContext(AuditService.name);
|
||||
}
|
||||
|
||||
@ -52,7 +49,7 @@ export class AuditService {
|
||||
|
||||
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
|
||||
const audits = await this.repository.getAfter(dto.after, {
|
||||
userIds: [userId],
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { parse } from 'node:path';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@ -11,21 +10,19 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
export class DownloadService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.logger.setContext(DownloadService.name);
|
||||
}
|
||||
|
||||
@ -76,7 +73,7 @@ export class DownloadService {
|
||||
}
|
||||
|
||||
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
|
||||
|
||||
const zip = this.storageRepository.createZipStream();
|
||||
const assets = await this.assetRepository.getByIds(dto.assetIds);
|
||||
@ -119,20 +116,20 @@ export class DownloadService {
|
||||
|
||||
if (dto.assetIds) {
|
||||
const assetIds = dto.assetIds;
|
||||
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
|
||||
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
|
||||
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
|
||||
}
|
||||
|
||||
if (dto.albumId) {
|
||||
const albumId = dto.albumId;
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
|
||||
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
const userId = dto.userId;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
|
||||
return usePagination(PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
|
||||
@ -7,18 +6,15 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IMemoryRepository) private repository: IMemoryRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async search(auth: AuthDto) {
|
||||
const memories = await this.repository.search(auth.user.id);
|
||||
@ -26,7 +22,7 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
const memory = await this.findOrFail(id);
|
||||
return mapMemory(memory);
|
||||
}
|
||||
@ -35,7 +31,11 @@ export class MemoryService {
|
||||
// TODO validate type/data combination
|
||||
|
||||
const assetIds = dto.assetIds || [];
|
||||
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds);
|
||||
const allowedAssetIds = await checkAccess(this.access, {
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: assetIds,
|
||||
});
|
||||
const memory = await this.repository.create({
|
||||
ownerId: auth.user.id,
|
||||
type: dto.type,
|
||||
@ -50,7 +50,7 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
|
||||
const memory = await this.repository.update({
|
||||
id,
|
||||
@ -63,14 +63,14 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
|
||||
const repos = { accessRepository: this.accessRepository, repository: this.repository };
|
||||
const repos = { access: this.access, bulk: this.repository };
|
||||
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
@ -82,9 +82,9 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
|
||||
const repos = { accessRepository: this.accessRepository, repository: this.repository };
|
||||
const repos = { access: this.access, bulk: this.repository };
|
||||
const results = await removeAssets(auth, repos, {
|
||||
parentId: id,
|
||||
assetIds: dto.ids,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { mapUser } from 'src/dtos/user.dto';
|
||||
@ -7,16 +6,14 @@ import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerService {
|
||||
private access: AccessCore;
|
||||
constructor(
|
||||
@Inject(IPartnerRepository) private repository: IPartnerRepository,
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
) {}
|
||||
|
||||
async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
|
||||
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
|
||||
@ -49,7 +46,7 @@ export class PartnerService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
|
||||
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
|
||||
|
||||
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ImageFormat } from 'src/config';
|
||||
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
@ -50,6 +49,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
@ -59,12 +59,11 @@ import { IsNull } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class PersonService {
|
||||
private access: AccessCore;
|
||||
private configCore: SystemConfigCore;
|
||||
private storageCore: StorageCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@ -77,7 +76,6 @@ export class PersonService {
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.logger.setContext(PersonService.name);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
this.storageCore = StorageCore.create(
|
||||
@ -114,7 +112,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
const person = await this.findOrFail(personId);
|
||||
const result: PersonResponseDto[] = [];
|
||||
const changeFeaturePhoto: string[] = [];
|
||||
@ -122,7 +120,7 @@ export class PersonService {
|
||||
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
||||
|
||||
for (const face of faces) {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
|
||||
if (person.faceAssetId === null) {
|
||||
changeFeaturePhoto.push(person.id);
|
||||
}
|
||||
@ -143,9 +141,8 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId);
|
||||
|
||||
await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
|
||||
const face = await this.repository.getFaceById(dto.id);
|
||||
const person = await this.findOrFail(personId);
|
||||
|
||||
@ -161,7 +158,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
|
||||
const faces = await this.repository.getFaces(dto.id);
|
||||
return faces.map((asset) => mapFaces(asset, auth));
|
||||
}
|
||||
@ -188,17 +185,17 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.repository.getStatistics(id);
|
||||
}
|
||||
|
||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
const person = await this.repository.getById(id);
|
||||
if (!person || !person.thumbnailPath) {
|
||||
throw new NotFoundException();
|
||||
@ -212,7 +209,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
const assets = await this.repository.getAssets(id);
|
||||
return assets.map((asset) => mapAsset(asset));
|
||||
}
|
||||
@ -227,13 +224,13 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
|
||||
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
|
||||
if (!face) {
|
||||
throw new BadRequestException('Invalid assetId for feature face');
|
||||
@ -587,13 +584,17 @@ export class PersonService {
|
||||
throw new BadRequestException('Cannot merge a person into themselves');
|
||||
}
|
||||
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
let primaryPerson = await this.findOrFail(id);
|
||||
const primaryName = primaryPerson.name || primaryPerson.id;
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
|
||||
const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds);
|
||||
const allowedIds = await checkAccess(this.access, {
|
||||
auth,
|
||||
permission: Permission.PERSON_MERGE,
|
||||
ids: mergeIds,
|
||||
});
|
||||
|
||||
for (const mergeId of mergeIds) {
|
||||
const hasAccess = allowedIds.has(mergeId);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
@ -8,18 +7,16 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
|
||||
) {
|
||||
this.logger.setContext(SessionService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
|
||||
async handleCleanup() {
|
||||
@ -47,7 +44,7 @@ export class SessionService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
||||
await this.sessionRepository.delete(id);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
@ -21,22 +20,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { OpenGraphTags } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class SharedLinkService {
|
||||
private access: AccessCore;
|
||||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
) {
|
||||
this.logger.setContext(SharedLinkService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
@ -69,7 +67,7 @@ export class SharedLinkService {
|
||||
if (!dto.albumId) {
|
||||
throw new BadRequestException('Invalid albumId');
|
||||
}
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
|
||||
break;
|
||||
}
|
||||
|
||||
@ -78,7 +76,7 @@ export class SharedLinkService {
|
||||
throw new BadRequestException('Invalid assetIds');
|
||||
}
|
||||
|
||||
await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
|
||||
|
||||
break;
|
||||
}
|
||||
@ -139,7 +137,11 @@ export class SharedLinkService {
|
||||
|
||||
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
|
||||
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
|
||||
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
|
||||
const allowedAssetIds = await checkAccess(this.access, {
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: notPresentAssetIds,
|
||||
});
|
||||
|
||||
const results: AssetIdsResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
|
||||
@ -7,18 +6,15 @@ import { Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class StackService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> {
|
||||
const stacks = await this.stackRepository.search({
|
||||
@ -30,7 +26,7 @@ export class StackService {
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
|
||||
const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
|
||||
|
||||
@ -40,13 +36,13 @@ export class StackService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.STACK_READ, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
return mapStack(stack, { auth });
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.STACK_UPDATE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
||||
throw new BadRequestException('Primary asset must be in the stack');
|
||||
@ -60,14 +56,14 @@ export class StackService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.STACK_DELETE, id);
|
||||
await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] });
|
||||
await this.stackRepository.delete(id);
|
||||
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []);
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.STACK_DELETE, dto.ids);
|
||||
await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
||||
await this.stackRepository.deleteAll(dto.ids);
|
||||
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
|
||||
@ -10,27 +9,24 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
import { setIsEqual } from 'src/utils/set';
|
||||
|
||||
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
||||
|
||||
export class SyncService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
@Inject(IAuditRepository) private auditRepository: IAuditRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
|
||||
// mobile implementation is faster if this is a single id
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
const assets = await this.assetRepository.getAllForUserFullSync({
|
||||
ownerId: userId,
|
||||
updatedUntil: dto.updatedUntil,
|
||||
@ -54,7 +50,7 @@ export class SyncService {
|
||||
return FULL_SYNC;
|
||||
}
|
||||
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds);
|
||||
await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
|
||||
|
||||
const limit = 10_000;
|
||||
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||
@ -7,18 +6,15 @@ import { Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
|
||||
export class TimelineService {
|
||||
private accessCore: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private repository: IAssetRepository,
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
) {
|
||||
this.accessCore = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
||||
await this.timeBucketChecks(auth, dto);
|
||||
@ -60,15 +56,15 @@ export class TimelineService {
|
||||
|
||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||
if (dto.albumId) {
|
||||
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
} else {
|
||||
dto.userId = dto.userId || auth.user.id;
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
||||
await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
||||
if (dto.isArchived !== false) {
|
||||
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
@ -8,23 +7,20 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
export class TrashService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
) {}
|
||||
|
||||
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
||||
const { ids } = dto;
|
||||
await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids);
|
||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_RESTORE, ids });
|
||||
await this.restoreAndSend(auth, ids);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { Permission } from 'src/enum';
|
||||
import { setIsSuperset } from 'src/utils/set';
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
|
||||
|
||||
export type GrantedRequest = {
|
||||
requested: Permission[];
|
||||
@ -13,3 +17,268 @@ export const isGranted = ({ requested, current }: GrantedRequest) => {
|
||||
|
||||
return setIsSuperset(new Set(current), new Set(requested));
|
||||
};
|
||||
|
||||
export type AccessRequest = {
|
||||
auth: AuthDto;
|
||||
permission: Permission;
|
||||
ids: Set<string> | string[];
|
||||
};
|
||||
|
||||
type SharedLinkAccessRequest = { sharedLink: SharedLinkEntity; permission: Permission; ids: Set<string> };
|
||||
type OtherAccessRequest = { auth: AuthDto; permission: Permission; ids: Set<string> };
|
||||
|
||||
export const requireUploadAccess = (auth: AuthDto | null): AuthDto => {
|
||||
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return auth;
|
||||
};
|
||||
|
||||
export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => {
|
||||
const allowedIds = await checkAccess(access, request);
|
||||
if (!setIsEqual(new Set(request.ids), allowedIds)) {
|
||||
throw new BadRequestException(`Not found or no ${request.permission} access`);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => {
|
||||
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
||||
if (idSet.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return auth.sharedLink
|
||||
? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet })
|
||||
: checkOtherAccess(access, { auth, permission, ids: idSet });
|
||||
};
|
||||
|
||||
const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => {
|
||||
const { sharedLink, permission, ids } = request;
|
||||
const sharedLinkId = sharedLink.id;
|
||||
|
||||
switch (permission) {
|
||||
case Permission.ASSET_READ: {
|
||||
return await access.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
return await access.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return sharedLink.allowUpload ? ids : new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
// TODO: fix this to not use sharedLink.userId for access control
|
||||
return await access.asset.checkOwnerAccess(sharedLink.userId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
return await access.album.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
|
||||
}
|
||||
|
||||
case Permission.ALBUM_ADD_ASSET: {
|
||||
return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set();
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => {
|
||||
const { auth, permission, ids } = request;
|
||||
|
||||
switch (permission) {
|
||||
// uses album id
|
||||
case Permission.ACTIVITY_CREATE: {
|
||||
return await access.activity.checkCreateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
// uses activity id
|
||||
case Permission.ACTIVITY_DELETE: {
|
||||
const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isAlbumOwner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_READ: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPDATE: {
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DELETE: {
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_RESTORE: {
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.VIEWER,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_ADD_ASSET: {
|
||||
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.EDITOR,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_UPDATE: {
|
||||
return await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DELETE: {
|
||||
return await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_SHARE: {
|
||||
return await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.VIEWER,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_REMOVE_ASSET: {
|
||||
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.album.checkSharedAlbumAccess(
|
||||
auth.user.id,
|
||||
setDifference(ids, isOwner),
|
||||
AlbumUserRole.EDITOR,
|
||||
);
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
}
|
||||
|
||||
case Permission.ARCHIVE_READ: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.AUTH_DEVICE_DELETE: {
|
||||
return await access.authDevice.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_READ: {
|
||||
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_DOWNLOAD: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.MEMORY_READ: {
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_UPDATE: {
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_DELETE: {
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_DELETE: {
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_UPDATE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_CREATE: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_REASSIGN: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PARTNER_UPDATE: {
|
||||
return await access.partner.checkUpdateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_READ: {
|
||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_UPDATE: {
|
||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_DELETE: {
|
||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { AssetFileType, Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { checkAccess } from 'src/utils/access';
|
||||
|
||||
export interface IBulkAsset {
|
||||
getAssetIds: (id: string, assetIds: string[]) => Promise<Set<string>>;
|
||||
@ -23,15 +23,17 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({
|
||||
|
||||
export const addAssets = async (
|
||||
auth: AuthDto,
|
||||
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
|
||||
repositories: { access: IAccessRepository; bulk: IBulkAsset },
|
||||
dto: { parentId: string; assetIds: string[] },
|
||||
) => {
|
||||
const { accessRepository, repository } = repositories;
|
||||
const access = AccessCore.create(accessRepository);
|
||||
|
||||
const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds);
|
||||
const { access, bulk } = repositories;
|
||||
const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds);
|
||||
const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id));
|
||||
const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
|
||||
const allowedAssetIds = await checkAccess(access, {
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: notPresentAssetIds,
|
||||
});
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
@ -53,7 +55,7 @@ export const addAssets = async (
|
||||
|
||||
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (newAssetIds.length > 0) {
|
||||
await repository.addAssetIds(dto.parentId, newAssetIds);
|
||||
await bulk.addAssetIds(dto.parentId, newAssetIds);
|
||||
}
|
||||
|
||||
return results;
|
||||
@ -61,18 +63,17 @@ export const addAssets = async (
|
||||
|
||||
export const removeAssets = async (
|
||||
auth: AuthDto,
|
||||
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
|
||||
repositories: { access: IAccessRepository; bulk: IBulkAsset },
|
||||
dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission },
|
||||
) => {
|
||||
const { accessRepository, repository } = repositories;
|
||||
const access = AccessCore.create(accessRepository);
|
||||
const { access, bulk } = repositories;
|
||||
|
||||
// check if the user can always remove from the parent album, memory, etc.
|
||||
const canAlwaysRemove = await access.checkAccess(auth, dto.canAlwaysRemove, [dto.parentId]);
|
||||
const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds);
|
||||
const canAlwaysRemove = await checkAccess(access, { auth, permission: dto.canAlwaysRemove, ids: [dto.parentId] });
|
||||
const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds);
|
||||
const allowedAssetIds = canAlwaysRemove.has(dto.parentId)
|
||||
? existingAssetIds
|
||||
: await access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds);
|
||||
: await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds });
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
@ -94,7 +95,7 @@ export const removeAssets = async (
|
||||
|
||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (removedIds.length > 0) {
|
||||
await repository.removeAssetIds(dto.parentId, removedIds);
|
||||
await bulk.removeAssetIds(dto.parentId, removedIds);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
@ -14,11 +13,7 @@ export interface IAccessRepositoryMock {
|
||||
timeline: Mocked<IAccessRepository['timeline']>;
|
||||
}
|
||||
|
||||
export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => {
|
||||
if (reset) {
|
||||
AccessCore.reset();
|
||||
}
|
||||
|
||||
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
return {
|
||||
activity: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
|
Loading…
Reference in New Issue
Block a user