mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
refactor(server)*: tsconfigs (#2689)
* refactor(server): tsconfigs * chore: dummy commit * fix: start.sh * chore: restore original entry scripts
This commit is contained in:
12
server/src/domain/shared-link/dto/create-shared-link.dto.ts
Normal file
12
server/src/domain/shared-link/dto/create-shared-link.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { AlbumEntity, AssetEntity, SharedLinkType } from '@app/infra/entities';
|
||||
|
||||
export class CreateSharedLinkDto {
|
||||
description?: string;
|
||||
expiresAt?: Date;
|
||||
type!: SharedLinkType;
|
||||
assets!: AssetEntity[];
|
||||
album?: AlbumEntity;
|
||||
allowUpload?: boolean;
|
||||
allowDownload?: boolean;
|
||||
showExif?: boolean;
|
||||
}
|
||||
18
server/src/domain/shared-link/dto/edit-shared-link.dto.ts
Normal file
18
server/src/domain/shared-link/dto/edit-shared-link.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class EditSharedLinkDto {
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
expiresAt?: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
allowUpload?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
allowDownload?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
showExif?: boolean;
|
||||
}
|
||||
2
server/src/domain/shared-link/dto/index.ts
Normal file
2
server/src/domain/shared-link/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-shared-link.dto';
|
||||
export * from './edit-shared-link.dto';
|
||||
5
server/src/domain/shared-link/index.ts
Normal file
5
server/src/domain/shared-link/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './dto';
|
||||
export * from './response-dto';
|
||||
export * from './shared-link.core';
|
||||
export * from './shared-link.service';
|
||||
export * from './shared-link.repository';
|
||||
1
server/src/domain/shared-link/response-dto/index.ts
Normal file
1
server/src/domain/shared-link/response-dto/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './shared-link-response.dto';
|
||||
@@ -0,0 +1,66 @@
|
||||
import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album';
|
||||
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset';
|
||||
|
||||
export class SharedLinkResponseDto {
|
||||
id!: string;
|
||||
description?: string;
|
||||
userId!: string;
|
||||
key!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
|
||||
type!: SharedLinkType;
|
||||
createdAt!: Date;
|
||||
expiresAt!: Date | null;
|
||||
assets!: AssetResponseDto[];
|
||||
album?: AlbumResponseDto;
|
||||
allowUpload!: boolean;
|
||||
allowDownload!: boolean;
|
||||
showExif!: boolean;
|
||||
}
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map(mapAsset),
|
||||
album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showExif: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map(mapAssetWithoutExif),
|
||||
album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showExif: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
80
server/src/domain/shared-link/shared-link.core.ts
Normal file
80
server/src/domain/shared-link/shared-link.core.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { AssetEntity, SharedLinkEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { ICryptoRepository } from '../crypto';
|
||||
import { CreateSharedLinkDto } from './dto';
|
||||
import { ISharedLinkRepository } from './shared-link.repository';
|
||||
|
||||
export class SharedLinkCore {
|
||||
readonly logger = new Logger(SharedLinkCore.name);
|
||||
|
||||
constructor(private repository: ISharedLinkRepository, private cryptoRepository: ICryptoRepository) {}
|
||||
|
||||
// TODO: move to SharedLinkController/SharedLinkService
|
||||
create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
|
||||
return this.repository.create({
|
||||
key: Buffer.from(this.cryptoRepository.randomBytes(50)),
|
||||
description: dto.description,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
expiresAt: dto.expiresAt ?? null,
|
||||
type: dto.type,
|
||||
assets: dto.assets,
|
||||
album: dto.album,
|
||||
allowUpload: dto.allowUpload ?? false,
|
||||
allowDownload: dto.allowDownload ?? true,
|
||||
showExif: dto.showExif ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
async addAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||
const link = await this.repository.get(userId, id);
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
return this.repository.update({ ...link, assets: [...link.assets, ...assets] });
|
||||
}
|
||||
|
||||
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||
const link = await this.repository.get(userId, id);
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
|
||||
|
||||
return this.repository.update({ ...link, assets: newAssets });
|
||||
}
|
||||
|
||||
checkDownloadAccess(user: AuthUserDto) {
|
||||
if (user.isPublicUser && !user.isAllowDownload) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
async validate(key: string | string[]): Promise<AuthUserDto | null> {
|
||||
key = Array.isArray(key) ? key[0] : key;
|
||||
|
||||
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
||||
const link = await this.repository.getByKey(bytes);
|
||||
if (link) {
|
||||
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
|
||||
const user = link.user;
|
||||
if (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
isAdmin: user.isAdmin,
|
||||
isPublicUser: true,
|
||||
sharedLinkId: link.id,
|
||||
isAllowUpload: link.allowUpload,
|
||||
isAllowDownload: link.allowDownload,
|
||||
isShowExif: link.showExif,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new UnauthorizedException('Invalid share key');
|
||||
}
|
||||
}
|
||||
12
server/src/domain/shared-link/shared-link.repository.ts
Normal file
12
server/src/domain/shared-link/shared-link.repository.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SharedLinkEntity } from '@app/infra/entities';
|
||||
|
||||
export const ISharedLinkRepository = 'ISharedLinkRepository';
|
||||
|
||||
export interface ISharedLinkRepository {
|
||||
getAll(userId: string): Promise<SharedLinkEntity[]>;
|
||||
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
|
||||
getByKey(key: Buffer): Promise<SharedLinkEntity | null>;
|
||||
create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>;
|
||||
update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
|
||||
remove(entity: SharedLinkEntity): Promise<void>;
|
||||
}
|
||||
103
server/src/domain/shared-link/shared-link.service.spec.ts
Normal file
103
server/src/domain/shared-link/shared-link.service.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import { authStub, newSharedLinkRepositoryMock, sharedLinkResponseStub, sharedLinkStub } from '@test';
|
||||
import { SharedLinkService } from './shared-link.service';
|
||||
import { ISharedLinkRepository } from './shared-link.repository';
|
||||
|
||||
describe(SharedLinkService.name, () => {
|
||||
let sut: SharedLinkService;
|
||||
let shareMock: jest.Mocked<ISharedLinkRepository>;
|
||||
|
||||
beforeEach(async () => {
|
||||
shareMock = newSharedLinkRepositoryMock();
|
||||
|
||||
sut = new SharedLinkService(shareMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all shared links for a user', async () => {
|
||||
shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
|
||||
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
|
||||
sharedLinkResponseStub.expired,
|
||||
sharedLinkResponseStub.valid,
|
||||
]);
|
||||
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMine', () => {
|
||||
it('should only work for a public user', async () => {
|
||||
await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(shareMock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the shared link for the public user', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||
});
|
||||
|
||||
it('should return not return exif', async () => {
|
||||
const authDto = authStub.adminSharedLinkNoExif;
|
||||
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoExif);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
shareMock.get.mockResolvedValue(null);
|
||||
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
|
||||
expect(shareMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get a shared link by id', async () => {
|
||||
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
shareMock.get.mockResolvedValue(null);
|
||||
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
|
||||
expect(shareMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a shared link', async () => {
|
||||
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
shareMock.update.mockResolvedValue(sharedLinkStub.valid);
|
||||
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
|
||||
expect(shareMock.update).toHaveBeenCalledWith({
|
||||
id: sharedLinkStub.valid.id,
|
||||
userId: authStub.user1.id,
|
||||
allowDownload: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
shareMock.get.mockResolvedValue(null);
|
||||
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
|
||||
expect(shareMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove a key', async () => {
|
||||
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
|
||||
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
|
||||
expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
|
||||
});
|
||||
});
|
||||
});
|
||||
63
server/src/domain/shared-link/shared-link.service.ts
Normal file
63
server/src/domain/shared-link/shared-link.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { SharedLinkEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { EditSharedLinkDto } from './dto';
|
||||
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
|
||||
import { ISharedLinkRepository } from './shared-link.repository';
|
||||
|
||||
@Injectable()
|
||||
export class SharedLinkService {
|
||||
constructor(@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository) {}
|
||||
|
||||
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
|
||||
}
|
||||
|
||||
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
||||
const { sharedLinkId: id, isPublicUser, isShowExif } = authUser;
|
||||
|
||||
if (!isPublicUser || !id) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const sharedLink = await this.findOrFail(authUser, id);
|
||||
|
||||
return this.map(sharedLink, { withExif: isShowExif ?? true });
|
||||
}
|
||||
|
||||
async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> {
|
||||
const sharedLink = await this.findOrFail(authUser, id);
|
||||
return this.map(sharedLink, { withExif: true });
|
||||
}
|
||||
|
||||
async update(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
|
||||
await this.findOrFail(authUser, id);
|
||||
const sharedLink = await this.repository.update({
|
||||
id,
|
||||
userId: authUser.id,
|
||||
description: dto.description,
|
||||
expiresAt: dto.expiresAt,
|
||||
allowUpload: dto.allowUpload,
|
||||
allowDownload: dto.allowDownload,
|
||||
showExif: dto.showExif,
|
||||
});
|
||||
return this.map(sharedLink, { withExif: true });
|
||||
}
|
||||
|
||||
async remove(authUser: AuthUserDto, id: string): Promise<void> {
|
||||
const sharedLink = await this.findOrFail(authUser, id);
|
||||
await this.repository.remove(sharedLink);
|
||||
}
|
||||
|
||||
private async findOrFail(authUser: AuthUserDto, id: string) {
|
||||
const sharedLink = await this.repository.get(authUser.id, id);
|
||||
if (!sharedLink) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
return sharedLink;
|
||||
}
|
||||
|
||||
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user