1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-21 15:36:26 +02:00

refactor(server): auth dto ()

* refactor: AuthUserDto => AuthDto

* refactor: reorganize auth-dto

* refactor: AuthUser() => Auth()
This commit is contained in:
Jason Rasmussen 2023-12-09 23:34:12 -05:00 committed by GitHub
parent 8057c375ba
commit 33529d1d9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1033 additions and 1065 deletions

View file

@ -1,5 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { SharedLinkEntity } from '../../infra/entities';
import { AuthDto } from '../auth';
import { setDifference, setIsEqual, setUnion } from '../domain.util';
import { IAccessRepository } from '../repositories';
@ -64,20 +65,20 @@ export class AccessCore {
instance = null;
}
requireUploadAccess(authUser: AuthUserDto | null): AuthUserDto {
if (!authUser || (authUser.isPublicUser && !authUser.isAllowUpload)) {
requireUploadAccess(auth: AuthDto | null): AuthDto {
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
throw new UnauthorizedException();
}
return authUser;
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(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) {
ids = Array.isArray(ids) ? ids : [ids];
const allowedIds = await this.checkAccess(authUser, permission, ids);
const allowedIds = await this.checkAccess(auth, permission, ids);
if (!setIsEqual(new Set(ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${permission} access`);
}
@ -89,23 +90,21 @@ export class AccessCore {
*
* @returns Set<string>
*/
async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set<string> | string[]) {
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]) {
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) {
return new Set();
}
const isSharedLink = authUser.isPublicUser ?? false;
return isSharedLink
? await this.checkAccessSharedLink(authUser, permission, idSet)
: await this.checkAccessOther(authUser, permission, idSet);
if (auth.sharedLink) {
return this.checkAccessSharedLink(auth.sharedLink, permission, idSet);
}
return this.checkAccessOther(auth, permission, idSet);
}
private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
const sharedLinkId = authUser.sharedLinkId;
if (!sharedLinkId) {
return new Set();
}
private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set<string>) {
const sharedLinkId = sharedLink.id;
switch (permission) {
case Permission.ASSET_READ:
@ -115,22 +114,22 @@ export class AccessCore {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload
return !!sharedLink.allowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
case Permission.ASSET_UPLOAD:
return authUser.isAllowUpload ? ids : new Set();
return sharedLink.allowUpload ? ids : new Set();
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
// 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 !!authUser.isAllowDownload
return !!sharedLink.allowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
@ -139,129 +138,129 @@ export class AccessCore {
}
}
private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
switch (permission) {
case Permission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
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(
authUser.id,
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_SHARE: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
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(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
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(
authUser.id,
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_DOWNLOAD: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
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(
authUser.id,
auth.user.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_UPDATE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
case Permission.ASSET_DELETE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
case Permission.ASSET_RESTORE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
case Permission.ALBUM_READ: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_UPDATE:
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
case Permission.ALBUM_DELETE:
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
case Permission.ALBUM_SHARE:
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
case Permission.ALBUM_DOWNLOAD: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_REMOVE_ASSET:
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
case Permission.ASSET_UPLOAD:
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
case Permission.ARCHIVE_READ:
return ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
case Permission.AUTH_DEVICE_DELETE:
return await this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
case Permission.TIMELINE_READ: {
const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set<string>();
const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
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(authUser.id) ? new Set([authUser.id]) : new Set();
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
case Permission.LIBRARY_READ: {
const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids);
const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.LIBRARY_UPDATE:
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
case Permission.LIBRARY_DELETE:
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
case Permission.PERSON_READ:
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
case Permission.PERSON_WRITE:
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
case Permission.PERSON_MERGE:
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
case Permission.PERSON_CREATE:
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
return this.repository.person.hasFaceOwnerAccess(auth.user.id, ids);
case Permission.PERSON_REASSIGN:
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
return this.repository.person.hasFaceOwnerAccess(auth.user.id, ids);
case Permission.PARTNER_UPDATE:
return await this.repository.partner.checkUpdateAccess(authUser.id, ids);
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
}
const allowedIds = new Set();
for (const id of ids) {
const hasAccess = await this.hasOtherAccess(authUser, permission, id);
const hasAccess = await this.hasOtherAccess(auth, permission, id);
if (hasAccess) {
allowedIds.add(id);
}
@ -270,17 +269,17 @@ export class AccessCore {
}
// TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk.
private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) {
private async hasOtherAccess(auth: AuthDto, permission: Permission, id: string) {
switch (permission) {
// uses album id
case Permission.ACTIVITY_CREATE:
return await this.repository.activity.hasCreateAccess(authUser.id, id);
return await this.repository.activity.hasCreateAccess(auth.user.id, id);
// uses activity id
case Permission.ACTIVITY_DELETE:
return (
(await this.repository.activity.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id))
(await this.repository.activity.hasOwnerAccess(auth.user.id, id)) ||
(await this.repository.activity.hasAlbumOwnerAccess(auth.user.id, id))
);
default:

View file

@ -1,7 +1,7 @@
import { ActivityEntity } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { IAccessRepository, IActivityRepository } from '../repositories';
import {
ActivityCreateDto,
@ -26,8 +26,8 @@ export class ActivityService {
this.access = AccessCore.create(accessRepository);
}
async getAll(authUser: AuthUserDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId);
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
const activities = await this.repository.search({
userId: dto.userId,
albumId: dto.albumId,
@ -38,16 +38,16 @@ export class ActivityService {
return activities.map(mapActivity);
}
async getStatistics(authUser: AuthUserDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId);
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
}
async create(authUser: AuthUserDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.access.requirePermission(authUser, Permission.ACTIVITY_CREATE, dto.albumId);
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId);
const common = {
userId: authUser.id,
userId: auth.user.id,
assetId: dto.assetId,
albumId: dto.albumId,
};
@ -77,8 +77,8 @@ export class ActivityService {
return { duplicate, value: mapActivity(activity) };
}
async delete(authUser: AuthUserDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.ACTIVITY_DELETE, id);
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id);
await this.repository.delete(id);
}
}

View file

@ -48,9 +48,9 @@ describe(AlbumService.name, () => {
notShared: 0,
});
expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.id);
expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.id);
expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.id);
expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.user.id);
expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.user.id);
expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.user.id);
});
});
@ -188,7 +188,7 @@ describe(AlbumService.name, () => {
});
expect(albumMock.create).toHaveBeenCalledWith({
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
albumName: albumStub.empty.albumName,
description: albumStub.empty.description,
sharedUsers: [{ id: 'user-id' }],
@ -312,7 +312,7 @@ describe(AlbumService.name, () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
await expect(
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }),
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.user.id] }),
).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled();
});
@ -332,11 +332,11 @@ describe(AlbumService.name, () => {
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
userMock.get.mockResolvedValue(userStub.user2);
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.id] });
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.user.id] });
expect(albumMock.update).toHaveBeenCalledWith({
id: albumStub.sharedWithAdmin.id,
updatedAt: expect.any(Date),
sharedUsers: [userStub.admin, { id: authStub.user2.id }],
sharedUsers: [userStub.admin, { id: authStub.user2.user.id }],
});
});
});
@ -370,12 +370,12 @@ describe(AlbumService.name, () => {
albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple);
await expect(
sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.id),
sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id),
).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled();
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.user1.id,
authStub.user1.user.id,
new Set([albumStub.sharedWithMultiple.id]),
);
});
@ -383,7 +383,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves', async () => {
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.id);
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id);
expect(albumMock.update).toHaveBeenCalledTimes(1);
expect(albumMock.update).toHaveBeenCalledWith({
@ -409,7 +409,7 @@ describe(AlbumService.name, () => {
it('should not allow the owner to be removed', async () => {
albumMock.getById.mockResolvedValue(albumStub.empty);
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.id)).rejects.toBeInstanceOf(
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.user.id)).rejects.toBeInstanceOf(
BadRequestException,
);
@ -444,7 +444,7 @@ describe(AlbumService.name, () => {
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.id,
authStub.admin.user.id,
new Set([albumStub.oneAsset.id]),
);
});
@ -465,7 +465,7 @@ describe(AlbumService.name, () => {
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
authStub.adminSharedLink.sharedLink?.id,
new Set(['album-123']),
);
});
@ -485,14 +485,20 @@ describe(AlbumService.name, () => {
await sut.get(authStub.user1, 'album-123', {});
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['album-123']));
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
authStub.user1.user.id,
new Set(['album-123']),
);
});
it('should throw an error for no access', async () => {
await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123']));
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123']));
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123']));
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set(['album-123']),
);
});
});
@ -590,7 +596,7 @@ describe(AlbumService.name, () => {
});
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
authStub.adminSharedLink.sharedLink?.id,
new Set(['album-123']),
);
});
@ -610,7 +616,7 @@ describe(AlbumService.name, () => {
updatedAt: expect.any(Date),
albumThumbnailAssetId: 'asset-1',
});
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
});
it('should skip duplicate assets', async () => {
@ -635,8 +641,8 @@ describe(AlbumService.name, () => {
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
]);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
});
it('should not allow unauthorized access to the album', async () => {
@ -729,7 +735,7 @@ describe(AlbumService.name, () => {
// // await expect(
// // sut.removeAssetsFromAlbum(
// // authUser,
// // auth,
// // {
// // ids: ['1'],
// // },
@ -755,6 +761,6 @@ describe(AlbumService.name, () => {
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
// albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
// await expect(sut.removeAssets(authUser, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
// await expect(sut.removeAssets(auth, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
// });
});

View file

@ -2,7 +2,7 @@ import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { setUnion } from '../domain.util';
import {
AlbumAssetCount,
@ -35,11 +35,11 @@ export class AlbumService {
this.access = AccessCore.create(accessRepository);
}
async getCount(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
async getCount(auth: AuthDto): Promise<AlbumCountResponseDto> {
const [owned, shared, notShared] = await Promise.all([
this.albumRepository.getOwned(authUser.id),
this.albumRepository.getShared(authUser.id),
this.albumRepository.getNotShared(authUser.id),
this.albumRepository.getOwned(auth.user.id),
this.albumRepository.getShared(auth.user.id),
this.albumRepository.getNotShared(auth.user.id),
]);
return {
@ -49,7 +49,7 @@ export class AlbumService {
};
}
async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
for (const albumId of invalidAlbumIds) {
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
@ -98,8 +98,8 @@ export class AlbumService {
);
}
async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets });
@ -113,7 +113,7 @@ export class AlbumService {
};
}
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
async create(auth: AuthDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
for (const userId of dto.sharedWithUserIds || []) {
const exists = await this.userRepository.get(userId, {});
if (!exists) {
@ -122,7 +122,7 @@ export class AlbumService {
}
const album = await this.albumRepository.create({
ownerId: authUser.id,
ownerId: auth.user.id,
albumName: dto.albumName,
description: dto.description,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [],
@ -133,8 +133,8 @@ export class AlbumService {
return mapAlbumWithAssets(album);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id);
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id);
const album = await this.findOrFail(id, { withAssets: true });
@ -155,22 +155,22 @@ export class AlbumService {
return mapAlbumWithoutAssets(updatedAlbum);
}
async delete(authUser: AuthUserDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id);
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
const album = await this.findOrFail(id, { withAssets: false });
await this.albumRepository.delete(album);
}
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id));
const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
const results: BulkIdResponseDto[] = [];
for (const assetId of dto.ids) {
@ -202,14 +202,14 @@ export class AlbumService {
return results;
}
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds);
const canRemove = await this.access.checkAccess(auth, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
const canShare = await this.access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds);
const allowedAssetIds = setUnion(canRemove, canShare);
const results: BulkIdResponseDto[] = [];
@ -241,8 +241,8 @@ export class AlbumService {
return results;
}
async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id);
async addUsers(auth: AuthDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
const album = await this.findOrFail(id, { withAssets: false });
@ -273,9 +273,9 @@ export class AlbumService {
.then(mapAlbumWithoutAssets);
}
async removeUser(authUser: AuthUserDto, id: string, userId: string | 'me'): Promise<void> {
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
if (userId === 'me') {
userId = authUser.id;
userId = auth.user.id;
}
const album = await this.findOrFail(id, { withAssets: false });
@ -290,8 +290,8 @@ export class AlbumService {
}
// non-admin can remove themselves
if (authUser.id !== userId) {
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id);
if (auth.user.id !== userId) {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
}
await this.albumRepository.update({

View file

@ -21,7 +21,7 @@ describe(APIKeyService.name, () => {
expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'Test Key',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
});
expect(cryptoMock.randomBytes).toHaveBeenCalled();
expect(cryptoMock.hashSha256).toHaveBeenCalled();
@ -35,7 +35,7 @@ describe(APIKeyService.name, () => {
expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'API Key',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
});
expect(cryptoMock.randomBytes).toHaveBeenCalled();
expect(cryptoMock.hashSha256).toHaveBeenCalled();
@ -59,7 +59,7 @@ describe(APIKeyService.name, () => {
await sut.update(authStub.admin, 'random-guid', { name: 'New Name' });
expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.id, 'random-guid', { name: 'New Name' });
expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid', { name: 'New Name' });
});
});
@ -77,7 +77,7 @@ describe(APIKeyService.name, () => {
await sut.delete(authStub.admin, 'random-guid');
expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.id, 'random-guid');
expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
});
});
@ -87,7 +87,7 @@ describe(APIKeyService.name, () => {
await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid');
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
});
it('should get a key by id', async () => {
@ -95,7 +95,7 @@ describe(APIKeyService.name, () => {
await sut.getById(authStub.admin, 'random-guid');
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid');
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
});
});
@ -105,7 +105,7 @@ describe(APIKeyService.name, () => {
await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1);
expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id);
expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
});
});
});

View file

@ -1,6 +1,6 @@
import { APIKeyEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { ICryptoRepository, IKeyRepository } from '../repositories';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
@ -11,47 +11,47 @@ export class APIKeyService {
@Inject(IKeyRepository) private repository: IKeyRepository,
) {}
async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
const entity = await this.repository.create({
key: this.crypto.hashSha256(secret),
name: dto.name || 'API Key',
userId: authUser.id,
userId: auth.user.id,
});
return { secret, apiKey: this.map(entity) };
}
async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(authUser.id, id);
async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(auth.user.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
}
const key = await this.repository.update(authUser.id, id, { name: dto.name });
const key = await this.repository.update(auth.user.id, id, { name: dto.name });
return this.map(key);
}
async delete(authUser: AuthUserDto, id: string): Promise<void> {
const exists = await this.repository.getById(authUser.id, id);
async delete(auth: AuthDto, id: string): Promise<void> {
const exists = await this.repository.getById(auth.user.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
}
await this.repository.delete(authUser.id, id);
await this.repository.delete(auth.user.id, id);
}
async getById(authUser: AuthUserDto, id: string): Promise<APIKeyResponseDto> {
const key = await this.repository.getById(authUser.id, id);
async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
const key = await this.repository.getById(auth.user.id, id);
if (!key) {
throw new BadRequestException('API Key not found');
}
return this.map(key);
}
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(authUser.id);
async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(auth.user.id);
return keys.map((key) => this.map(key));
}

View file

@ -59,7 +59,7 @@ const statResponse: AssetStatsResponseDto = {
const uploadFile = {
nullAuth: {
authUser: null,
auth: null,
fieldName: UploadFieldName.ASSET_DATA,
file: {
checksum: Buffer.from('checksum', 'utf8'),
@ -69,7 +69,7 @@ const uploadFile = {
},
filename: (fieldName: UploadFieldName, filename: string) => {
return {
authUser: authStub.admin,
auth: authStub.admin,
fieldName,
file: {
mimeType: 'image/jpeg',
@ -328,7 +328,7 @@ describe(AssetService.name, () => {
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] },
]);
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.id, { day: 15, month: 1 }]]);
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.user.id, { day: 15, month: 1 }]]);
});
});
@ -341,7 +341,7 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY,
}),
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.id] });
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id] });
});
});
@ -354,7 +354,7 @@ describe(AssetService.name, () => {
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-id']));
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
@ -370,14 +370,14 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userIds: [authStub.admin.id],
userIds: [authStub.admin.user.id],
});
});
@ -388,13 +388,13 @@ describe(AssetService.name, () => {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userIds: [authStub.admin.id],
userIds: [authStub.admin.user.id],
});
});
@ -405,7 +405,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket',
isArchived: true,
withPartners: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).rejects.toThrowError(BadRequestException);
@ -415,7 +415,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket',
isArchived: undefined,
withPartners: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).rejects.toThrowError(BadRequestException);
});
@ -427,7 +427,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket',
isFavorite: true,
withPartners: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).rejects.toThrowError(BadRequestException);
@ -437,7 +437,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket',
isFavorite: false,
withPartners: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).rejects.toThrowError(BadRequestException);
});
@ -449,7 +449,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket',
isTrashed: true,
withPartners: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
}),
).rejects.toThrowError(BadRequestException);
});
@ -459,9 +459,9 @@ describe(AssetService.name, () => {
it('should require the asset.download permission', async () => {
await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
});
it('should throw an error if the asset is not found', async () => {
@ -550,28 +550,28 @@ describe(AssetService.name, () => {
await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse);
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-1']));
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-1']));
expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
});
it('should return a list of archives (userId)', async () => {
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id]));
assetMock.getByUserId.mockResolvedValue({
items: [assetStub.image, assetStub.video],
hasNextPage: false,
});
await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.id })).resolves.toEqual(
await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.user.id })).resolves.toEqual(
downloadResponse,
);
expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.id, {
expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.user.id, {
isVisible: true,
});
});
it('should split archives by size', async () => {
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id]));
assetMock.getByUserId.mockResolvedValue({
items: [
@ -585,7 +585,7 @@ describe(AssetService.name, () => {
await expect(
sut.getDownloadInfo(authStub.admin, {
userId: authStub.admin.id,
userId: authStub.admin.user.id,
archiveSize: 30_000,
}),
).resolves.toEqual({
@ -624,25 +624,25 @@ describe(AssetService.name, () => {
it('should get the statistics for a user, excluding archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: false });
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
});
it('should get the statistics for a user for archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: true });
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
});
it('should get the statistics for a user for favorite assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isFavorite: true });
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true });
});
it('should get the statistics for a user for all assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {});
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {});
});
});
@ -762,7 +762,7 @@ describe(AssetService.name, () => {
stackParentId: 'parent',
});
expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.id, [
expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.user.id, [
'asset-1',
]);
});

View file

@ -5,7 +5,7 @@ import { DateTime, Duration } from 'luxon';
import { extname } from 'path';
import sanitize from 'sanitize-filename';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util';
import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
@ -63,7 +63,7 @@ export enum UploadFieldName {
}
export interface UploadRequest {
authUser: AuthUserDto | null;
auth: AuthDto | null;
fieldName: UploadFieldName;
file: UploadFile;
}
@ -93,7 +93,7 @@ export class AssetService {
this.configCore = SystemConfigCore.create(configRepository);
}
search(authUser: AuthUserDto, dto: AssetSearchDto) {
search(auth: AuthDto, dto: AssetSearchDto) {
let checksum: Buffer | undefined = undefined;
if (dto.checksum) {
@ -109,7 +109,7 @@ export class AssetService {
...dto,
order,
checksum,
ownerId: authUser.id,
ownerId: auth.user.id,
})
.then((assets) =>
assets.map((asset) =>
@ -121,8 +121,8 @@ export class AssetService {
);
}
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(authUser);
canUploadFile({ auth, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(auth);
const filename = file.originalName;
@ -156,8 +156,8 @@ export class AssetService {
throw new BadRequestException(`Unsupported file type ${filename}`);
}
getUploadFilename({ authUser, fieldName, file }: UploadRequest): string {
this.access.requireUploadAccess(authUser);
getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
this.access.requireUploadAccess(auth);
const originalExt = extname(file.originalName);
@ -171,12 +171,12 @@ export class AssetService {
return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`);
}
getUploadFolder({ authUser, fieldName }: UploadRequest): string {
authUser = this.access.requireUploadAccess(authUser);
getUploadFolder({ auth, fieldName }: UploadRequest): string {
auth = this.access.requireUploadAccess(auth);
let folder = StorageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id);
let folder = StorageCore.getFolderLocation(StorageFolder.UPLOAD, auth.user.id);
if (fieldName === UploadFieldName.PROFILE_DATA) {
folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id);
folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, auth.user.id);
}
this.storageRepository.mkdirSync(folder);
@ -184,13 +184,13 @@ export class AssetService {
return folder;
}
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.assetRepository.getMapMarkers(authUser.id, options);
getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.assetRepository.getMapMarkers(auth.user.id, options);
}
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const currentYear = new Date().getFullYear();
const assets = await this.assetRepository.getByDayOfYear(authUser.id, dto);
const assets = await this.assetRepository.getByDayOfYear(auth.user.id, dto);
return _.chain(assets)
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
@ -207,17 +207,17 @@ export class AssetService {
.value();
}
private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) {
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]);
await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
} else {
dto.userId = dto.userId || authUser.id;
dto.userId = dto.userId || auth.user.id;
}
if (dto.userId) {
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, [dto.userId]);
await this.access.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
if (dto.isArchived !== false) {
await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]);
await this.access.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
}
}
@ -234,28 +234,28 @@ export class AssetService {
}
}
async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(authUser, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto);
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions);
}
async getTimeBucket(
authUser: AuthUserDto,
auth: AuthDto,
dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(authUser, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto);
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
if (authUser.isShowMetadata) {
if (!auth.sharedLink || auth.sharedLink?.showExif) {
return assets.map((asset) => mapAsset(asset, { withStack: true }));
} else {
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
}
}
async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto;
let userIds: string[] | undefined = undefined;
@ -263,7 +263,7 @@ export class AssetService {
userIds = [userId];
if (dto.withPartners) {
const partners = await this.partnerRepository.getAll(authUser.id);
const partners = await this.partnerRepository.getAll(auth.user.id);
const partnersIds = partners
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
.map((partner) => partner.sharedById);
@ -274,8 +274,8 @@ export class AssetService {
return { ...options, userIds };
}
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);
async downloadFile(auth: AuthDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id);
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
@ -289,12 +289,12 @@ export class AssetService {
return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath));
}
async getDownloadInfo(authUser: AuthUserDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
const archives: DownloadArchiveInfo[] = [];
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
const assetPagination = await this.getDownloadAssets(authUser, dto);
const assetPagination = await this.getDownloadAssets(auth, dto);
for await (const assets of assetPagination) {
// motion part of live photos
const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id);
@ -323,8 +323,8 @@ export class AssetService {
};
}
async downloadArchive(authUser: AuthUserDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds);
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds);
const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds);
@ -347,12 +347,12 @@ export class AssetService {
return { stream: zip.stream };
}
private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadInfoDto): Promise<AsyncGenerator<AssetEntity[]>> {
private async getDownloadAssets(auth: AuthDto, dto: DownloadInfoDto): Promise<AsyncGenerator<AssetEntity[]>> {
const PAGINATION_SIZE = 2500;
if (dto.assetIds) {
const assetIds = dto.assetIds;
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, assetIds);
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds);
const assets = await this.assetRepository.getByIds(assetIds);
return (async function* () {
yield assets;
@ -361,13 +361,13 @@ export class AssetService {
if (dto.albumId) {
const albumId = dto.albumId;
await this.access.requirePermission(authUser, Permission.ALBUM_DOWNLOAD, albumId);
await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId);
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
}
if (dto.userId) {
const userId = dto.userId;
await this.access.requirePermission(authUser, Permission.TIMELINE_DOWNLOAD, userId);
await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId);
return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
);
@ -376,22 +376,22 @@ export class AssetService {
throw new BadRequestException('assetIds, albumId, or userId is required');
}
async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) {
const stats = await this.assetRepository.getStatistics(authUser.id, dto);
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
return mapStats(stats);
}
async getRandom(authUser: AuthUserDto, count: number): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.getRandom(authUser.id, count);
async getRandom(auth: AuthDto, count: number): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.getRandom(auth.user.id, count);
return assets.map((a) => mapAsset(a));
}
async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
return this.assetRepository.getAllByDeviceId(authUser.id, deviceId);
async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
@ -400,9 +400,9 @@ export class AssetService {
return mapAsset(asset);
}
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids);
if (removeParent) {
(options as Partial<AssetEntity>).stackParentId = null;
@ -411,7 +411,7 @@ export class AssetService {
// All the unique parent's -> parent is set to null
ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!)));
} else if (options.stackParentId) {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, options.stackParentId);
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId);
// Merge stacks
const assets = await this.assetRepository.getByIds(ids);
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0);
@ -430,7 +430,7 @@ export class AssetService {
}
await this.assetRepository.updateAll(ids, options);
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, auth.user.id, ids);
}
async handleAssetDeletionCheck() {
@ -493,10 +493,10 @@ export class AssetService {
return true;
}
async deleteAll(authUser: AuthUserDto, dto: AssetBulkDeleteDto): Promise<void> {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_DELETE, ids);
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
if (force) {
for (const id of ids) {
@ -504,20 +504,20 @@ export class AssetService {
}
} else {
await this.assetRepository.softDeleteAll(ids);
this.communicationRepository.send(CommunicationEvent.ASSET_TRASH, authUser.id, ids);
this.communicationRepository.send(CommunicationEvent.ASSET_TRASH, auth.user.id, ids);
}
}
async handleTrashAction(authUser: AuthUserDto, action: TrashAction): Promise<void> {
async handleTrashAction(auth: AuthDto, action: TrashAction): Promise<void> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, authUser.id, { trashedBefore: DateTime.now().toJSDate() }),
this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }),
);
if (action == TrashAction.RESTORE_ALL) {
for await (const assets of assetPagination) {
const ids = assets.map((a) => a.id);
await this.assetRepository.restoreAll(ids);
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, auth.user.id, ids);
}
return;
}
@ -532,17 +532,17 @@ export class AssetService {
}
}
async restoreAll(authUser: AuthUserDto, dto: BulkIdsDto): Promise<void> {
async restoreAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_RESTORE, ids);
await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids);
await this.assetRepository.restoreAll(ids);
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, auth.user.id, ids);
}
async updateStackParent(authUser: AuthUserDto, dto: UpdateStackParentDto): Promise<void> {
async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise<void> {
const { oldParentId, newParentId } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_READ, oldParentId);
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, newParentId);
await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId);
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId);
const childIds: string[] = [];
const oldParent = await this.assetRepository.getById(oldParentId);
@ -552,14 +552,14 @@ export class AssetService {
childIds.push(...(oldParent.stack?.map((a) => a.id) ?? []));
}
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, [...childIds, newParentId]);
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, auth.user.id, [...childIds, newParentId]);
await this.assetRepository.updateAll(childIds, { stackParentId: newParentId });
// Remove ParentId of new parent if this was previously a child of some other asset
return this.assetRepository.updateAll([newParentId], { stackParentId: null });
}
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
async run(auth: AuthDto, dto: AssetJobsDto) {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds);
for (const id of dto.assetIds) {
switch (dto.name) {

View file

@ -65,7 +65,7 @@ describe(AuditService.name, () => {
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
action: DatabaseAction.DELETE,
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
entityType: EntityType.ASSET,
});
});
@ -81,7 +81,7 @@ describe(AuditService.name, () => {
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
action: DatabaseAction.DELETE,
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
entityType: EntityType.ASSET,
});
});

View file

@ -3,7 +3,7 @@ import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
import { usePagination } from '../domain.util';
import { JOBS_ASSET_PAGINATION_SIZE } from '../job';
@ -48,9 +48,9 @@ export class AuditService {
return true;
}
async getDeletes(authUser: AuthUserDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || authUser.id;
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId);
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const audits = await this.repository.getAfter(dto.after, {
ownerId: userId,

View file

@ -1,19 +1,14 @@
import { UserEntity, UserTokenEntity } from '@app/infra/entities';
import { APIKeyEntity, SharedLinkEntity, UserEntity, UserTokenEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class AuthUserDto {
id!: string;
email!: string;
isAdmin!: boolean;
isPublicUser?: boolean;
sharedLinkId?: string;
isAllowUpload?: boolean;
isAllowDownload?: boolean;
isShowMetadata?: boolean;
accessTokenId?: string;
externalPath?: string | null;
export class AuthDto {
user!: UserEntity;
apiKey?: APIKeyEntity;
sharedLink?: SharedLinkEntity;
userToken?: UserTokenEntity;
}
export class LoginCredentialDto {

View file

@ -31,7 +31,7 @@ import {
IUserTokenRepository,
} from '../repositories';
import { AuthType } from './auth.constant';
import { AuthUserDto, SignUpDto } from './auth.dto';
import { AuthDto, SignUpDto } from './auth.dto';
import { AuthService } from './auth.service';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -145,7 +145,7 @@ describe('AuthService', () => {
describe('changePassword', () => {
it('should change the password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue({
@ -153,23 +153,23 @@ describe('AuthService', () => {
password: 'hash-password',
} as UserEntity);
await sut.changePassword(authUser, dto);
await sut.changePassword(auth, dto);
expect(userMock.getByEmail).toHaveBeenCalledWith(authUser.email, true);
expect(userMock.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
});
it('should throw when auth user email is not found', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue(null);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(UnauthorizedException);
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should throw when password does not match existing password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const auth = { user: { email: 'test@imimch.com' } as UserEntity };
const dto = { password: 'old-password', newPassword: 'new-password' };
cryptoMock.compareBcrypt.mockReturnValue(false);
@ -179,11 +179,11 @@ describe('AuthService', () => {
password: 'hash-password',
} as UserEntity);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
});
it('should throw when user does not have a password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue({
@ -191,33 +191,33 @@ describe('AuthService', () => {
password: '',
} as UserEntity);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('logout', () => {
it('should return the end session endpoint', async () => {
configMock.load.mockResolvedValue(systemConfigStub.enabled);
const authUser = { id: '123' } as AuthUserDto;
await expect(sut.logout(authUser, AuthType.OAUTH)).resolves.toEqual({
const auth = { user: { id: '123' } } as AuthDto;
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
successful: true,
redirectUri: 'http://end-session-endpoint',
});
});
it('should return the default redirect', async () => {
const authUser = { id: '123' } as AuthUserDto;
const auth = { user: { id: '123' } } as AuthDto;
await expect(sut.logout(authUser, AuthType.PASSWORD)).resolves.toEqual({
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
});
it('should delete the access token', async () => {
const authUser = { id: '123', accessTokenId: 'token123' } as AuthUserDto;
const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto;
await expect(sut.logout(authUser, AuthType.PASSWORD)).resolves.toEqual({
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
@ -226,9 +226,9 @@ describe('AuthService', () => {
});
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
const authUser = { id: '123' } as AuthUserDto;
const auth = { user: { id: '123' } } as AuthDto;
await expect(sut.logout(authUser, AuthType.OAUTH)).resolves.toEqual({
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
@ -268,7 +268,10 @@ describe('AuthService', () => {
userMock.get.mockResolvedValue(userStub.user1);
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual(userStub.user1);
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
user: userStub.user1,
userToken: userTokenStub.userToken,
});
});
});
@ -296,7 +299,10 @@ describe('AuthService', () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') };
await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink);
await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.admin,
sharedLink: sharedLinkStub.valid,
});
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
});
@ -304,7 +310,10 @@ describe('AuthService', () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') };
await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink);
await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.admin,
sharedLink: sharedLinkStub.valid,
});
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
});
});
@ -319,14 +328,20 @@ describe('AuthService', () => {
it('should return an auth dto', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(userStub.user1);
await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.user1,
userToken: userTokenStub.userToken,
});
});
it('should update when access time exceeds an hour', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken);
userTokenMock.save.mockResolvedValue(userTokenStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(userStub.user1);
await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.user1,
userToken: userTokenStub.userToken,
});
expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({
id: 'not_active',
token: 'auth_token',
@ -350,7 +365,7 @@ describe('AuthService', () => {
it('should return an auth dto', async () => {
keyMock.getKey.mockResolvedValue(keyStub.admin);
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(authStub.admin);
await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin });
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
});
});
@ -377,7 +392,7 @@ describe('AuthService', () => {
},
]);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});
@ -387,7 +402,7 @@ describe('AuthService', () => {
await sut.logoutDevices(authStub.user1);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
expect(userTokenMock.delete).toHaveBeenCalledWith('not_active');
expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id');
});
@ -399,7 +414,7 @@ describe('AuthService', () => {
await sut.logoutDevice(authStub.user1, 'token-1');
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1']));
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
});
});
@ -506,7 +521,7 @@ describe('AuthService', () => {
await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.id, { oauthId: sub });
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: sub });
});
it('should not link an already linked oauth.sub', async () => {
@ -528,7 +543,7 @@ describe('AuthService', () => {
await sut.unlink(authStub.user1);
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.id, { oauthId: '' });
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' });
});
});
});

View file

@ -34,7 +34,7 @@ import {
} from './auth.constant';
import {
AuthDeviceResponseDto,
AuthUserDto,
AuthDto,
ChangePasswordDto,
LoginCredentialDto,
LoginResponseDto,
@ -110,9 +110,9 @@ export class AuthService {
return this.createLoginResponse(user, AuthType.PASSWORD, details);
}
async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
if (authUser.accessTokenId) {
await this.userTokenRepository.delete(authUser.accessTokenId);
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
if (auth.userToken) {
await this.userTokenRepository.delete(auth.userToken.id);
}
return {
@ -121,9 +121,9 @@ export class AuthService {
};
}
async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) {
async changePassword(auth: AuthDto, dto: ChangePasswordDto) {
const { password, newPassword } = dto;
const user = await this.userRepository.getByEmail(authUser.email, true);
const user = await this.userRepository.getByEmail(auth.user.email, true);
if (!user) {
throw new UnauthorizedException();
}
@ -133,7 +133,7 @@ export class AuthService {
throw new BadRequestException('Wrong password');
}
return this.userCore.updateUser(authUser, authUser.id, { password: newPassword });
return this.userCore.updateUser(auth.user, auth.user.id, { password: newPassword });
}
async adminSignUp(dto: SignUpDto): Promise<UserResponseDto> {
@ -154,7 +154,7 @@ export class AuthService {
return mapUser(admin);
}
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto> {
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
const userToken = (headers['x-immich-user-token'] ||
params.userToken ||
@ -177,20 +177,20 @@ export class AuthService {
throw new UnauthorizedException('Authentication required');
}
async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenRepository.getAll(authUser.id);
return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId));
async getDevices(auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenRepository.getAll(auth.user.id);
return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id));
}
async logoutDevice(authUser: AuthUserDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.AUTH_DEVICE_DELETE, id);
async logoutDevice(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
await this.userTokenRepository.delete(id);
}
async logoutDevices(authUser: AuthUserDto): Promise<void> {
const devices = await this.userTokenRepository.getAll(authUser.id);
async logoutDevices(auth: AuthDto): Promise<void> {
const devices = await this.userTokenRepository.getAll(auth.user.id);
for (const device of devices) {
if (device.id === authUser.accessTokenId) {
if (device.id === auth.userToken?.id) {
continue;
}
await this.userTokenRepository.delete(device.id);
@ -284,19 +284,19 @@ export class AuthService {
return this.createLoginResponse(user, AuthType.OAUTH, loginDetails);
}
async link(user: AuthUserDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
const config = await this.configCore.getConfig();
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
const duplicate = await this.userRepository.getByOAuthId(oauthId);
if (duplicate && duplicate.id !== user.id) {
if (duplicate && duplicate.id !== auth.user.id) {
this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
throw new BadRequestException('This OAuth account has already been linked to another user.');
}
return mapUser(await this.userRepository.update(user.id, { oauthId }));
return mapUser(await this.userRepository.update(auth.user.id, { oauthId }));
}
async unlink(user: AuthUserDto): Promise<UserResponseDto> {
return mapUser(await this.userRepository.update(user.id, { oauthId: '' }));
async unlink(auth: AuthDto): Promise<UserResponseDto> {
return mapUser(await this.userRepository.update(auth.user.id, { oauthId: '' }));
}
private async getLogoutEndpoint(authType: AuthType): Promise<string> {
@ -371,45 +371,27 @@ export class AuthService {
return cookies[IMMICH_ACCESS_COOKIE] || null;
}
private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> {
private async validateSharedLink(key: string | string[]): Promise<AuthDto> {
key = Array.isArray(key) ? key[0] : key;
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
const link = await this.sharedLinkRepository.getByKey(bytes);
if (link) {
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
const user = link.user;
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
if (sharedLink) {
if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) {
const user = sharedLink.user;
if (user) {
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: true,
sharedLinkId: link.id,
isAllowUpload: link.allowUpload,
isAllowDownload: link.allowDownload,
isShowMetadata: link.showExif,
};
return { user, sharedLink };
}
}
}
throw new UnauthorizedException('Invalid share key');
}
private async validateApiKey(key: string): Promise<AuthUserDto> {
private async validateApiKey(key: string): Promise<AuthDto> {
const hashedKey = this.cryptoRepository.hashSha256(key);
const keyEntity = await this.keyRepository.getKey(hashedKey);
if (keyEntity?.user) {
const user = keyEntity.user;
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: false,
isAllowUpload: true,
externalPath: user.externalPath,
};
const apiKey = await this.keyRepository.getKey(hashedKey);
if (apiKey?.user) {
return { user: apiKey.user, apiKey };
}
throw new UnauthorizedException('Invalid API key');
@ -422,26 +404,19 @@ export class AuthService {
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
}
private async validateUserToken(tokenValue: string): Promise<AuthUserDto> {
private async validateUserToken(tokenValue: string): Promise<AuthDto> {
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
let token = await this.userTokenRepository.getByToken(hashedToken);
let userToken = await this.userTokenRepository.getByToken(hashedToken);
if (token?.user) {
if (userToken?.user) {
const now = DateTime.now();
const updatedAt = DateTime.fromJSDate(token.updatedAt);
const updatedAt = DateTime.fromJSDate(userToken.updatedAt);
const diff = now.diff(updatedAt, ['hours']);
if (diff.hours > 1) {
token = await this.userTokenRepository.save({ ...token, updatedAt: new Date() });
userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() });
}
return {
...token.user,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowMetadata: true,
accessTokenId: token.id,
};
return { user: userToken.user, userToken };
}
throw new UnauthorizedException('Invalid user token');

View file

@ -632,7 +632,7 @@ describe(LibraryService.name, () => {
await expect(sut.getCount(authStub.admin)).resolves.toBe(17);
expect(libraryMock.getCountForUser).toHaveBeenCalledWith(authStub.admin.id);
expect(libraryMock.getCountForUser).toHaveBeenCalledWith(authStub.admin.user.id);
});
});
@ -673,7 +673,7 @@ describe(LibraryService.name, () => {
}),
]);
expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.id);
expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
});
});
@ -963,10 +963,10 @@ describe(LibraryService.name, () => {
describe('update', () => {
it('can update library ', async () => {
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1);
await expect(sut.update(authStub.admin, authStub.admin.id, {})).resolves.toBeTruthy();
await expect(sut.update(authStub.admin, authStub.admin.user.id, {})).resolves.toBeTruthy();
expect(libraryMock.update).toHaveBeenCalledWith(
expect.objectContaining({
id: authStub.admin.id,
id: authStub.admin.user.id,
}),
);
});

View file

@ -5,7 +5,7 @@ import { Stats } from 'node:fs';
import path from 'node:path';
import { basename, parse } from 'path';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant';
import { usePagination, validateCronExpression } from '../domain.util';
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
@ -70,22 +70,22 @@ export class LibraryService {
});
}
async getStatistics(authUser: AuthUserDto, id: string): Promise<LibraryStatsResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id);
async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
return this.repository.getStatistics(id);
}
async getCount(authUser: AuthUserDto): Promise<number> {
return this.repository.getCountForUser(authUser.id);
async getCount(auth: AuthDto): Promise<number> {
return this.repository.getCountForUser(auth.user.id);
}
async getAllForUser(authUser: AuthUserDto): Promise<LibraryResponseDto[]> {
const libraries = await this.repository.getAllByUserId(authUser.id);
async getAllForUser(auth: AuthDto): Promise<LibraryResponseDto[]> {
const libraries = await this.repository.getAllByUserId(auth.user.id);
return libraries.map((library) => mapLibrary(library));
}
async get(authUser: AuthUserDto, id: string): Promise<LibraryResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id);
async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
const library = await this.findOrFail(id);
return mapLibrary(library);
}
@ -99,7 +99,7 @@ export class LibraryService {
return true;
}
async create(authUser: AuthUserDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
switch (dto.type) {
case LibraryType.EXTERNAL:
if (!dto.name) {
@ -120,7 +120,7 @@ export class LibraryService {
}
const library = await this.repository.create({
ownerId: authUser.id,
ownerId: auth.user.id,
name: dto.name,
type: dto.type,
importPaths: dto.importPaths ?? [],
@ -131,17 +131,17 @@ export class LibraryService {
return mapLibrary(library);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id);
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
const library = await this.repository.update({ id, ...dto });
return mapLibrary(library);
}
async delete(authUser: AuthUserDto, id: string) {
await this.access.requirePermission(authUser, Permission.LIBRARY_DELETE, id);
async delete(auth: AuthDto, id: string) {
await this.access.requirePermission(auth, Permission.LIBRARY_DELETE, id);
const library = await this.findOrFail(id);
const uploadCount = await this.repository.getUploadLibraryCount(authUser.id);
const uploadCount = await this.repository.getUploadLibraryCount(auth.user.id);
if (library.type === LibraryType.UPLOAD && uploadCount <= 1) {
throw new BadRequestException('Cannot delete the last upload library');
}
@ -294,8 +294,8 @@ export class LibraryService {
return true;
}
async queueScan(authUser: AuthUserDto, id: string, dto: ScanLibraryDto) {
await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id);
async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) {
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
const library = await this.repository.get(id);
if (!library || library.type !== LibraryType.EXTERNAL) {
@ -312,9 +312,9 @@ export class LibraryService {
});
}
async queueRemoveOffline(authUser: AuthUserDto, id: string) {
async queueRemoveOffline(auth: AuthDto, id: string) {
this.logger.verbose(`Removing offline files from library: ${id}`);
await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id);
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
await this.jobRepository.queue({
name: JobName.LIBRARY_REMOVE_OFFLINE,

View file

@ -60,13 +60,13 @@ describe(PartnerService.name, () => {
it("should return a list of partners with whom I've shared my library", async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toEqual([responseDto.admin]);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
it('should return a list of partners who have shared their libraries with me', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toEqual([responseDto.admin]);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});
@ -75,18 +75,18 @@ describe(PartnerService.name, () => {
partnerMock.get.mockResolvedValue(null);
partnerMock.create.mockResolvedValue(partnerStub.adminToUser1);
await expect(sut.create(authStub.admin, authStub.user1.id)).resolves.toEqual(responseDto.user1);
await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toEqual(responseDto.user1);
expect(partnerMock.create).toHaveBeenCalledWith({
sharedById: authStub.admin.id,
sharedWithId: authStub.user1.id,
sharedById: authStub.admin.user.id,
sharedWithId: authStub.user1.user.id,
});
});
it('should throw an error when the partner already exists', async () => {
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1);
await expect(sut.create(authStub.admin, authStub.user1.id)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.create(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException);
expect(partnerMock.create).not.toHaveBeenCalled();
});
@ -96,7 +96,7 @@ describe(PartnerService.name, () => {
it('should remove a partner', async () => {
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1);
await sut.remove(authStub.admin, authStub.user1.id);
await sut.remove(authStub.admin, authStub.user1.user.id);
expect(partnerMock.remove).toHaveBeenCalledWith(partnerStub.adminToUser1);
});
@ -104,7 +104,7 @@ describe(PartnerService.name, () => {
it('should throw an error when the partner does not exist', async () => {
partnerMock.get.mockResolvedValue(null);
await expect(sut.remove(authStub.admin, authStub.user1.id)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.remove(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException);
expect(partnerMock.remove).not.toHaveBeenCalled();
});

View file

@ -1,7 +1,7 @@
import { PartnerEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories';
import { mapUser } from '../user';
import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto';
@ -16,8 +16,8 @@ export class PartnerService {
this.access = AccessCore.create(accessRepository);
}
async create(authUser: AuthUserDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId };
async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const exists = await this.repository.get(partnerId);
if (exists) {
throw new BadRequestException(`Partner already exists`);
@ -27,8 +27,8 @@ export class PartnerService {
return this.map(partner, PartnerDirection.SharedBy);
}
async remove(authUser: AuthUserDto, sharedWithId: string): Promise<void> {
const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId };
async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const partner = await this.repository.get(partnerId);
if (!partner) {
throw new BadRequestException('Partner not found');
@ -37,18 +37,18 @@ export class PartnerService {
await this.repository.remove(partner);
}
async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(authUser.id);
async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(auth.user.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
.filter((partner) => partner[key] === authUser.id)
.filter((partner) => partner[key] === auth.user.id)
.map((partner) => this.map(partner, direction));
}
async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById);
const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id };
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById);
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
return this.map(entity, PartnerDirection.SharedWith);

View file

@ -2,7 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { Optional, ValidateUUID, toBoolean } from '../domain.util';
export class PersonUpdateDto {
@ -156,9 +156,9 @@ export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPe
};
}
export function mapFaces(face: AssetFaceEntity, authUser: AuthUserDto): AssetFaceResponseDto {
export function mapFaces(face: AssetFaceEntity, auth: AuthDto): AssetFaceResponseDto {
return {
...mapFacesWithoutPerson(face),
person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null,
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
};
}

View file

@ -113,7 +113,7 @@ describe(PersonService.name, () => {
visible: 1,
people: [responseDto],
});
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 1,
withHidden: false,
});
@ -125,7 +125,7 @@ describe(PersonService.name, () => {
visible: 1,
people: [responseDto],
});
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 1,
withHidden: false,
});
@ -146,7 +146,7 @@ describe(PersonService.name, () => {
},
],
});
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 1,
withHidden: true,
});
@ -157,14 +157,14 @@ describe(PersonService.name, () => {
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.withName);
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw a bad request when person is not found', async () => {
personMock.getById.mockResolvedValue(null);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should get a person by id', async () => {
@ -172,7 +172,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto);
expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -181,7 +181,7 @@ describe(PersonService.name, () => {
personMock.getById.mockResolvedValue(personStub.noName);
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw an error when personId is invalid', async () => {
@ -189,7 +189,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw an error when person has no thumbnail', async () => {
@ -197,7 +197,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should serve the thumbnail', async () => {
@ -205,7 +205,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getThumbnail(authStub.admin, 'person-1');
expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg');
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -214,7 +214,7 @@ describe(PersonService.name, () => {
personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(personMock.getAssets).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it("should return a person's assets", async () => {
@ -222,7 +222,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getAssets(authStub.admin, 'person-1');
expect(personMock.getAssets).toHaveBeenCalledWith('person-1');
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -233,7 +233,7 @@ describe(PersonService.name, () => {
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw an error when personId is invalid', async () => {
@ -243,7 +243,7 @@ describe(PersonService.name, () => {
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it("should update a person's name", async () => {
@ -256,7 +256,7 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' });
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it("should update a person's date of birth", async () => {
@ -276,7 +276,7 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
expect(jobMock.queue).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should update a person visibility', async () => {
@ -289,7 +289,7 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false });
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it("should update a person's thumbnailPath", async () => {
@ -312,7 +312,7 @@ describe(PersonService.name, () => {
},
]);
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } });
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw an error when the face feature assetId is invalid', async () => {
@ -323,7 +323,7 @@ describe(PersonService.name, () => {
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -336,7 +336,7 @@ describe(PersonService.name, () => {
sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
expect(personMock.update).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -761,7 +761,7 @@ describe(PersonService.name, () => {
expect(personMock.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should merge two people', async () => {
@ -784,7 +784,7 @@ describe(PersonService.name, () => {
name: JobName.PERSON_DELETE,
data: { id: personStub.mergePerson.id },
});
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should throw an error when the primary person is not found', async () => {
@ -796,7 +796,7 @@ describe(PersonService.name, () => {
);
expect(personMock.delete).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should handle invalid merge ids', async () => {
@ -811,7 +811,7 @@ describe(PersonService.name, () => {
expect(personMock.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should handle an error reassigning faces', async () => {
@ -826,7 +826,7 @@ describe(PersonService.name, () => {
]);
expect(personMock.delete).not.toHaveBeenCalled();
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
@ -836,19 +836,19 @@ describe(PersonService.name, () => {
personMock.getStatistics.mockResolvedValue(statistics);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
});
});
describe('mapFace', () => {
it('should map a face', () => {
expect(mapFaces(faceStub.face1, personStub.withName.owner)).toEqual({
expect(mapFaces(faceStub.face1, { user: personStub.withName.owner })).toEqual({
boundingBoxX1: 0,
boundingBoxX2: 1,
boundingBoxY1: 0,

View file

@ -3,7 +3,7 @@ import { PersonPathType } from '@app/infra/entities/move.entity';
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant';
import { usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
@ -65,9 +65,9 @@ export class PersonService {
this.storageCore = StorageCore.create(assetRepository, moveRepository, repository, storageRepository);
}
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
const { machineLearning } = await this.configCore.getConfig();
const people = await this.repository.getAllForUser(authUser.id, {
const people = await this.repository.getAllForUser(auth.user.id, {
minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden: dto.withHidden || false,
});
@ -83,12 +83,12 @@ export class PersonService {
};
}
createPerson(authUser: AuthUserDto): Promise<PersonResponseDto> {
return this.repository.create({ ownerId: authUser.id });
createPerson(auth: AuthDto): Promise<PersonResponseDto> {
return this.repository.create({ ownerId: auth.user.id });
}
async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
const person = await this.findOrFail(personId);
const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = [];
@ -96,7 +96,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(authUser, Permission.PERSON_CREATE, face.id);
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id);
if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id);
}
@ -116,10 +116,10 @@ export class PersonService {
return result;
}
async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id);
await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id);
const face = await this.repository.getFaceById(dto.id);
const person = await this.findOrFail(personId);
@ -134,10 +134,10 @@ export class PersonService {
return await this.findOrFail(personId).then(mapPerson);
}
async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id);
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id);
const faces = await this.repository.getFaces(dto.id);
return faces.map((asset) => mapFaces(asset, authUser));
return faces.map((asset) => mapFaces(asset, auth));
}
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
@ -163,18 +163,18 @@ export class PersonService {
}
}
async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
return this.findOrFail(id).then(mapPerson);
}
async getStatistics(authUser: AuthUserDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
return this.repository.getStatistics(id);
}
async getThumbnail(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
const person = await this.repository.getById(id);
if (!person || !person.thumbnailPath) {
throw new NotFoundException();
@ -183,14 +183,14 @@ export class PersonService {
return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath));
}
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
const assets = await this.repository.getAssets(id);
return assets.map((asset) => mapAsset(asset));
}
async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
let person = await this.findOrFail(id);
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
@ -200,7 +200,7 @@ export class PersonService {
}
if (assetId) {
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
if (!face) {
throw new BadRequestException('Invalid assetId for feature face');
@ -213,11 +213,11 @@ export class PersonService {
return mapPerson(person);
}
async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
async updatePeople(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
const results: BulkIdResponseDto[] = [];
for (const person of dto.people) {
try {
await this.update(authUser, person.id, {
await this.update(auth, person.id, {
isHidden: person.isHidden,
name: person.name,
birthDate: person.birthDate,
@ -438,15 +438,15 @@ export class PersonService {
return true;
}
async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
const mergeIds = dto.ids;
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
const primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = [];
const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds);
const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds);
for (const mergeId of mergeIds) {
const hasAccess = allowedIds.has(mergeId);

View file

@ -49,11 +49,11 @@ describe(SearchService.name, () => {
await sut.searchPerson(authStub.user1, { name, withHidden: false });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.id, name, { withHidden: false });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
await sut.searchPerson(authStub.user1, { name, withHidden: true });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.id, name, { withHidden: true });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true });
});
});
@ -105,7 +105,7 @@ describe(SearchService.name, () => {
const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse);
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, authStub.user1.id, { numResults: 250 });
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, authStub.user1.user.id, { numResults: 250 });
expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled();
});
@ -132,7 +132,11 @@ describe(SearchService.name, () => {
const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse);
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ ownerId: authStub.user1.id, embedding, numResults: 100 });
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({
ownerId: authStub.user1.user.id,
embedding,
numResults: 100,
});
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
});

View file

@ -1,7 +1,7 @@
import { AssetEntity } from '@app/infra/entities';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { AssetResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { PersonResponseDto } from '../person';
import {
IAssetRepository,
@ -31,16 +31,16 @@ export class SearchService {
this.configCore = SystemConfigCore.create(configRepository);
}
async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(authUser.id, dto.name, { withHidden: dto.withHidden });
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
}
async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const options = { maxFields: 12, minAssetsPerField: 5 };
const results = await Promise.all([
this.assetRepository.getAssetIdByCity(authUser.id, options),
this.assetRepository.getAssetIdByTag(authUser.id, options),
this.assetRepository.getAssetIdByCity(auth.user.id, options),
this.assetRepository.getAssetIdByTag(auth.user.id, options),
]);
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
const assets = await this.assetRepository.getByIds(Array.from(assetIds));
@ -52,7 +52,7 @@ export class SearchService {
}));
}
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
const { machineLearning } = await this.configCore.getConfig();
const query = dto.q || dto.query;
if (!query) {
@ -73,10 +73,10 @@ export class SearchService {
{ text: query },
machineLearning.clip,
);
assets = await this.smartInfoRepository.searchCLIP({ ownerId: authUser.id, embedding, numResults: 100 });
assets = await this.smartInfoRepository.searchCLIP({ ownerId: auth.user.id, embedding, numResults: 100 });
break;
case SearchStrategy.TEXT:
assets = await this.assetRepository.searchMetadata(query, authUser.id, { numResults: 250 });
assets = await this.assetRepository.searchMetadata(query, auth.user.id, { numResults: 250 });
default:
break;
}

View file

@ -41,7 +41,7 @@ describe(SharedLinkService.name, () => {
sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid,
]);
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});
@ -55,21 +55,21 @@ describe(SharedLinkService.name, () => {
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);
expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});
it('should not return metadata', async () => {
const authDto = authStub.adminSharedLinkNoExif;
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});
it('should throw an error for an password protected shared link', async () => {
const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});
});
@ -77,14 +77,14 @@ describe(SharedLinkService.name, () => {
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.get).toHaveBeenCalledWith(authStub.user1.user.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);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
});
});
@ -120,12 +120,12 @@ describe(SharedLinkService.name, () => {
await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.id,
authStub.admin.user.id,
new Set([albumStub.oneAsset.id]),
);
expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.ALBUM,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
albumId: albumStub.oneAsset.id,
allowDownload: true,
allowUpload: true,
@ -149,10 +149,13 @@ describe(SharedLinkService.name, () => {
allowUpload: true,
});
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.INDIVIDUAL,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
albumId: null,
allowDownload: true,
allowUpload: true,
@ -169,7 +172,7 @@ describe(SharedLinkService.name, () => {
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.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled();
});
@ -177,10 +180,10 @@ describe(SharedLinkService.name, () => {
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.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(shareMock.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id,
userId: authStub.user1.id,
userId: authStub.user1.user.id,
allowDownload: false,
});
});
@ -190,14 +193,14 @@ describe(SharedLinkService.name, () => {
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.get).toHaveBeenCalledWith(authStub.user1.user.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.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
});
});

View file

@ -2,7 +2,7 @@ import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entiti
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto';
import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto';
@ -19,42 +19,36 @@ export class SharedLinkService {
this.access = AccessCore.create(accessRepository);
}
getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink));
}
async getMine(authUser: AuthUserDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser;
if (!isPublicUser || !id) {
async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
if (!auth.sharedLink) {
throw new ForbiddenException();
}
const sharedLink = await this.findOrFail(authUser, id);
let newToken;
const sharedLink = await this.findOrFail(auth, auth.sharedLink.id);
const response = this.map(sharedLink, { withExif: sharedLink.showExif });
if (sharedLink.password) {
newToken = this.validateAndRefreshToken(sharedLink, dto);
response.token = this.validateAndRefreshToken(sharedLink, dto);
}
return {
...this.map(sharedLink, { withExif: isShowExif ?? true }),
token: newToken,
};
return response;
}
async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> {
const sharedLink = await this.findOrFail(authUser, id);
async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
const sharedLink = await this.findOrFail(auth, id);
return this.map(sharedLink, { withExif: true });
}
async create(authUser: AuthUserDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
switch (dto.type) {
case SharedLinkType.ALBUM:
if (!dto.albumId) {
throw new BadRequestException('Invalid albumId');
}
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, dto.albumId);
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId);
break;
case SharedLinkType.INDIVIDUAL:
@ -62,14 +56,14 @@ export class SharedLinkService {
throw new BadRequestException('Invalid assetIds');
}
await this.access.requirePermission(authUser, Permission.ASSET_SHARE, dto.assetIds);
await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds);
break;
}
const sharedLink = await this.repository.create({
key: this.cryptoRepository.randomBytes(50),
userId: authUser.id,
userId: auth.user.id,
type: dto.type,
albumId: dto.albumId || null,
assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity),
@ -84,11 +78,11 @@ export class SharedLinkService {
return this.map(sharedLink, { withExif: true });
}
async update(authUser: AuthUserDto, id: string, dto: SharedLinkEditDto) {
await this.findOrFail(authUser, id);
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
await this.findOrFail(auth, id);
const sharedLink = await this.repository.update({
id,
userId: authUser.id,
userId: auth.user.id,
description: dto.description,
password: dto.password,
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
@ -99,21 +93,21 @@ export class SharedLinkService {
return this.map(sharedLink, { withExif: true });
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
const sharedLink = await this.findOrFail(authUser, id);
async remove(auth: AuthDto, id: string): Promise<void> {
const sharedLink = await this.findOrFail(auth, id);
await this.repository.remove(sharedLink);
}
private async findOrFail(authUser: AuthUserDto, id: string) {
const sharedLink = await this.repository.get(authUser.id, id);
private async findOrFail(auth: AuthDto, id: string) {
const sharedLink = await this.repository.get(auth.user.id, id);
if (!sharedLink) {
throw new BadRequestException('Shared link not found');
}
return sharedLink;
}
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(authUser, id);
async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(auth, id);
if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
throw new BadRequestException('Invalid shared link type');
@ -121,7 +115,7 @@ 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(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {
@ -146,8 +140,8 @@ export class SharedLinkService {
return results;
}
async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(authUser, id);
async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(auth, id);
if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
throw new BadRequestException('Invalid shared link type');

View file

@ -23,7 +23,7 @@ describe(TagService.name, () => {
it('should return all tags for a user', async () => {
tagMock.getAll.mockResolvedValue([tagStub.tag1]);
await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.tag1]);
expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.id);
expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.user.id);
});
});
@ -31,13 +31,13 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null);
await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
});
it('should return a tag for a user', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
});
});
@ -47,7 +47,7 @@ describe(TagService.name, () => {
await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.create).not.toHaveBeenCalled();
});
@ -57,7 +57,7 @@ describe(TagService.name, () => {
tagResponseStub.tag1,
);
expect(tagMock.create).toHaveBeenCalledWith({
userId: authStub.admin.id,
userId: authStub.admin.user.id,
name: 'tag-1',
type: TagType.CUSTOM,
});
@ -68,7 +68,7 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null);
await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.remove).not.toHaveBeenCalled();
});
@ -76,7 +76,7 @@ describe(TagService.name, () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
tagMock.update.mockResolvedValue(tagStub.tag1);
await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.tag1);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', name: 'tag-2' });
});
});
@ -85,14 +85,14 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null);
await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.remove).not.toHaveBeenCalled();
});
it('should remove a tag', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
await sut.remove(authStub.admin, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1);
});
});
@ -101,7 +101,7 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null);
await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.remove).not.toHaveBeenCalled();
});
@ -109,8 +109,8 @@ describe(TagService.name, () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
tagMock.getAssets.mockResolvedValue([assetStub.image]);
await sut.getAssets(authStub.admin, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
});
});
@ -120,15 +120,15 @@ describe(TagService.name, () => {
await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.addAssets).not.toHaveBeenCalled();
});
it('should reject duplicate asset ids and accept new ones', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false);
when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-1').mockResolvedValue(true);
when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-2').mockResolvedValue(false);
await expect(
sut.addAssets(authStub.admin, 'tag-1', {
@ -139,9 +139,9 @@ describe(TagService.name, () => {
{ assetId: 'asset-2', success: true },
]);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.hasAsset).toHaveBeenCalledTimes(2);
expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-2']);
expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-2']);
});
});
@ -151,15 +151,15 @@ describe(TagService.name, () => {
await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.removeAssets).not.toHaveBeenCalled();
});
it('should accept accept ids that are tagged and reject the rest', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false);
when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-1').mockResolvedValue(true);
when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-2').mockResolvedValue(false);
await expect(
sut.removeAssets(authStub.admin, 'tag-1', {
@ -170,9 +170,9 @@ describe(TagService.name, () => {
{ assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND },
]);
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1');
expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
expect(tagMock.hasAsset).toHaveBeenCalledTimes(2);
expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-1']);
expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-1']);
});
});
});

View file

@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { ITagRepository } from '../repositories';
import { TagResponseDto, mapTag } from './tag-response.dto';
import { CreateTagDto, UpdateTagDto } from './tag.dto';
@ -9,23 +9,23 @@ import { CreateTagDto, UpdateTagDto } from './tag.dto';
export class TagService {
constructor(@Inject(ITagRepository) private repository: ITagRepository) {}
getAll(authUser: AuthUserDto) {
return this.repository.getAll(authUser.id).then((tags) => tags.map(mapTag));
getAll(auth: AuthDto) {
return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag));
}
async getById(authUser: AuthUserDto, id: string): Promise<TagResponseDto> {
const tag = await this.findOrFail(authUser, id);
async getById(auth: AuthDto, id: string): Promise<TagResponseDto> {
const tag = await this.findOrFail(auth, id);
return mapTag(tag);
}
async create(authUser: AuthUserDto, dto: CreateTagDto) {
const duplicate = await this.repository.hasName(authUser.id, dto.name);
async create(auth: AuthDto, dto: CreateTagDto) {
const duplicate = await this.repository.hasName(auth.user.id, dto.name);
if (duplicate) {
throw new BadRequestException(`A tag with that name already exists`);
}
const tag = await this.repository.create({
userId: authUser.id,
userId: auth.user.id,
name: dto.name,
type: dto.type,
});
@ -33,29 +33,29 @@ export class TagService {
return mapTag(tag);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateTagDto): Promise<TagResponseDto> {
await this.findOrFail(authUser, id);
async update(auth: AuthDto, id: string, dto: UpdateTagDto): Promise<TagResponseDto> {
await this.findOrFail(auth, id);
const tag = await this.repository.update({ id, name: dto.name });
return mapTag(tag);
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
const tag = await this.findOrFail(authUser, id);
async remove(auth: AuthDto, id: string): Promise<void> {
const tag = await this.findOrFail(auth, id);
await this.repository.remove(tag);
}
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
await this.findOrFail(authUser, id);
const assets = await this.repository.getAssets(authUser.id, id);
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.findOrFail(auth, id);
const assets = await this.repository.getAssets(auth.user.id, id);
return assets.map((asset) => mapAsset(asset));
}
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(authUser, id);
async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(auth, id);
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {
const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId);
const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId);
if (hasAsset) {
results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
} else {
@ -64,7 +64,7 @@ export class TagService {
}
await this.repository.addAssets(
authUser.id,
auth.user.id,
id,
results.filter((result) => result.success).map((result) => result.assetId),
);
@ -72,12 +72,12 @@ export class TagService {
return results;
}
async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(authUser, id);
async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(auth, id);
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {
const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId);
const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId);
if (!hasAsset) {
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
} else {
@ -86,7 +86,7 @@ export class TagService {
}
await this.repository.removeAssets(
authUser.id,
auth.user.id,
id,
results.filter((result) => result.success).map((result) => result.assetId),
);
@ -94,8 +94,8 @@ export class TagService {
return results;
}
private async findOrFail(authUser: AuthUserDto, id: string) {
const tag = await this.repository.getById(authUser.id, id);
private async findOrFail(auth: AuthDto, id: string) {
const tag = await this.repository.getById(auth.user.id, id);
if (!tag) {
throw new BadRequestException('Tag not found');
}

View file

@ -2,8 +2,8 @@ import { LibraryType, UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import path from 'path';
import sanitize from 'sanitize-filename';
import { AuthUserDto } from '../auth';
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
import { UserResponseDto } from './response-dto';
const SALT_ROUNDS = 10;
@ -32,17 +32,18 @@ export class UserCore {
instance = null;
}
async updateUser(authUser: AuthUserDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
if (!authUser.isAdmin && authUser.id !== id) {
// TODO: move auth related checks to the service layer
async updateUser(user: UserEntity | UserResponseDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
if (!user.isAdmin && user.id !== id) {
throw new ForbiddenException('You are not allowed to update this user');
}
if (!authUser.isAdmin) {
if (!user.isAdmin) {
// Users can never update the isAdmin property.
delete dto.isAdmin;
delete dto.storageLabel;
delete dto.externalPath;
} else if (dto.isAdmin && authUser.id !== id) {
} else if (dto.isAdmin && user.id !== id) {
// Admin cannot create another admin.
throw new BadRequestException('The server already has an admin');
}

View file

@ -60,10 +60,10 @@ describe(UserService.name, () => {
sut = new UserService(albumMock, assetMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, userMock);
when(userMock.get).calledWith(authStub.admin.id, {}).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.user1.id, {}).mockResolvedValue(userStub.user1);
when(userMock.get).calledWith(authStub.user1.id, { withDeleted: true }).mockResolvedValue(userStub.user1);
when(userMock.get).calledWith(authStub.admin.user.id, {}).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.admin.user.id, { withDeleted: true }).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.user1.user.id, {}).mockResolvedValue(userStub.user1);
when(userMock.get).calledWith(authStub.user1.user.id, { withDeleted: true }).mockResolvedValue(userStub.user1);
});
describe('getAll', () => {
@ -71,8 +71,8 @@ describe(UserService.name, () => {
userMock.getList.mockResolvedValue([userStub.admin]);
await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([
expect.objectContaining({
id: authStub.admin.id,
email: authStub.admin.email,
id: authStub.admin.user.id,
email: authStub.admin.user.email,
}),
]);
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
@ -82,14 +82,14 @@ describe(UserService.name, () => {
describe('get', () => {
it('should get a user by id', async () => {
userMock.get.mockResolvedValue(userStub.admin);
await sut.get(authStub.admin.id);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false });
await sut.get(authStub.admin.user.id);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
});
it('should throw an error if a user is not found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.get(authStub.admin.id)).rejects.toBeInstanceOf(NotFoundException);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false });
await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(NotFoundException);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
});
});
@ -97,13 +97,13 @@ describe(UserService.name, () => {
it("should get the auth user's info", async () => {
userMock.get.mockResolvedValue(userStub.admin);
await sut.getMe(authStub.admin);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {});
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {});
});
it('should throw an error if a user is not found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.getMe(authStub.admin)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {});
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {});
});
});
@ -119,7 +119,7 @@ describe(UserService.name, () => {
userMock.getByStorageLabel.mockResolvedValue(null);
userMock.update.mockResolvedValue(userStub.user1);
await sut.update({ ...authStub.user1, isAdmin: true }, update);
await sut.update({ user: { ...authStub.user1.user, isAdmin: true } }, update);
expect(userMock.getByEmail).toHaveBeenCalledWith(update.email);
expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel);
@ -127,13 +127,16 @@ describe(UserService.name, () => {
it('should not set an empty string for storage label', async () => {
userMock.update.mockResolvedValue(userStub.user1);
await sut.update(userStub.admin, { id: userStub.user1.id, storageLabel: '' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id, storageLabel: null });
await sut.update(authStub.admin, { id: userStub.user1.id, storageLabel: '' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: userStub.user1.id,
storageLabel: null,
});
});
it('should omit a storage label set by non-admin users', async () => {
userMock.update.mockResolvedValue(userStub.user1);
await sut.update(userStub.user1, { id: userStub.user1.id, storageLabel: 'admin' });
await sut.update({ user: userStub.user1 }, { id: userStub.user1.id, storageLabel: 'admin' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id });
});
@ -145,10 +148,13 @@ describe(UserService.name, () => {
id: 'not_immich_auth_user_id',
});
const result = sut.update(userStub.user1, {
id: 'not_immich_auth_user_id',
password: 'I take over your account now',
});
const result = sut.update(
{ user: userStub.user1 },
{
id: 'not_immich_auth_user_id',
password: 'I take over your account now',
},
);
await expect(result).rejects.toBeInstanceOf(ForbiddenException);
});
@ -158,7 +164,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await sut.update(userStub.user1, dto);
await sut.update({ user: userStub.user1 }, dto);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id',
@ -172,7 +178,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.getByEmail.mockResolvedValue(userStub.admin);
await expect(sut.update(userStub.user1, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.update({ user: userStub.user1 }, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.update).not.toHaveBeenCalled();
});
@ -183,7 +189,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.getByStorageLabel.mockResolvedValue(userStub.admin);
await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.update(authStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.update).not.toHaveBeenCalled();
});
@ -195,7 +201,7 @@ describe(UserService.name, () => {
};
when(userMock.update).calledWith(userStub.user1.id, update).mockResolvedValueOnce(userStub.user1);
await sut.update(userStub.admin, update);
await sut.update(authStub.admin, update);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id',
shouldChangePassword: true,
@ -205,7 +211,7 @@ describe(UserService.name, () => {
it('update user information should throw error if user not found', async () => {
when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(null);
const result = sut.update(userStub.admin, {
const result = sut.update(authStub.admin, {
id: userStub.user1.id,
shouldChangePassword: true,
});
@ -218,7 +224,7 @@ describe(UserService.name, () => {
when(userMock.update).calledWith(userStub.admin.id, dto).mockResolvedValueOnce(userStub.admin);
await sut.update(userStub.admin, dto);
await sut.update(authStub.admin, dto);
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, dto);
});
@ -228,7 +234,7 @@ describe(UserService.name, () => {
when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(userStub.user1);
await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.update(authStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException);
});
});
@ -239,11 +245,6 @@ describe(UserService.name, () => {
expect(userMock.restore).not.toHaveBeenCalled();
});
it('should require an admin', async () => {
when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin);
await expect(sut.restore(authStub.user1, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException);
});
it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.mockResolvedValue(userStub.user1);
@ -267,7 +268,7 @@ describe(UserService.name, () => {
});
it('should require the auth user be an admin', async () => {
await expect(sut.delete(authStub.user1, authStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.delete(authStub.user1, authStub.admin.user.id)).rejects.toBeInstanceOf(ForbiddenException);
expect(userMock.delete).not.toHaveBeenCalled();
});
@ -276,7 +277,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.delete.mockResolvedValue(userStub.user1);
await expect(sut.delete(userStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
await expect(sut.delete(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {});
expect(userMock.delete).toHaveBeenCalledWith(userStub.user1);
});
@ -323,7 +324,7 @@ describe(UserService.name, () => {
const file = { path: '/profile/path' } as Express.Multer.File;
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException);
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(BadRequestException);
});
it('should throw an error if the user profile could not be updated with the new image', async () => {
@ -331,7 +332,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.profilePath);
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
});
it('should delete the previous profile image', async () => {
@ -340,7 +341,7 @@ describe(UserService.name, () => {
const files = [userStub.profilePath.profileImagePath];
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
await sut.createProfileImage(userStub.admin, file);
await sut.createProfileImage(authStub.admin, file);
await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
});
@ -349,7 +350,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.admin);
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
await sut.createProfileImage(userStub.admin, file);
await sut.createProfileImage(authStub.admin, file);
expect(jobMock.queue).not.toHaveBeenCalled();
});
});
@ -358,7 +359,7 @@ describe(UserService.name, () => {
it('should send an http error has no profile image', async () => {
userMock.get.mockResolvedValue(userStub.admin);
await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.deleteProfileImage(authStub.admin)).rejects.toBeInstanceOf(BadRequestException);
expect(jobMock.queue).not.toHaveBeenCalled();
});
@ -366,7 +367,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.profilePath);
const files = [userStub.profilePath.profileImagePath];
await sut.deleteProfileImage(userStub.admin);
await sut.deleteProfileImage(authStub.admin);
await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
});
});

View file

@ -1,7 +1,7 @@
import { UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'crypto';
import { AuthUserDto } from '../auth';
import { AuthDto } from '../auth';
import { IEntityJob, JobName } from '../job';
import {
IAlbumRepository,
@ -36,7 +36,7 @@ export class UserService {
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
}
async getAll(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: !isAll });
return users.map(mapUser);
}
@ -50,24 +50,20 @@ export class UserService {
return mapUser(user);
}
getMe(authUser: AuthUserDto): Promise<UserResponseDto> {
return this.findOrFail(authUser.id, {}).then(mapUser);
getMe(auth: AuthDto): Promise<UserResponseDto> {
return this.findOrFail(auth.user.id, {}).then(mapUser);
}
create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.userCore.createUser(createUserDto).then(mapUser);
}
async update(authUser: AuthUserDto, dto: UpdateUserDto): Promise<UserResponseDto> {
async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
await this.findOrFail(dto.id, {});
return this.userCore.updateUser(authUser, dto.id, dto).then(mapUser);
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
}
async delete(authUser: AuthUserDto, id: string): Promise<UserResponseDto> {
if (!authUser.isAdmin) {
throw new ForbiddenException('Unauthorized');
}
async delete(auth: AuthDto, id: string): Promise<UserResponseDto> {
const user = await this.findOrFail(id, {});
if (user.isAdmin) {
throw new ForbiddenException('Cannot delete admin user');
@ -78,35 +74,28 @@ export class UserService {
return this.userRepository.delete(user).then(mapUser);
}
async restore(authUser: AuthUserDto, id: string): Promise<UserResponseDto> {
if (!authUser.isAdmin) {
throw new ForbiddenException('Unauthorized');
}
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
let user = await this.findOrFail(id, { withDeleted: true });
user = await this.userRepository.restore(user);
await this.albumRepository.restoreAll(id);
return mapUser(user);
}
async createProfileImage(
authUser: AuthUserDto,
fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> {
const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false });
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path });
if (oldpath !== '') {
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
}
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
}
async deleteProfileImage(authUser: AuthUserDto): Promise<void> {
const user = await this.findOrFail(authUser.id, { withDeleted: false });
async deleteProfileImage(auth: AuthDto): Promise<void> {
const user = await this.findOrFail(auth.user.id, { withDeleted: false });
if (user.profileImagePath === '') {
throw new BadRequestException("Can't delete a missing profile Image");
}
await this.userRepository.update(authUser.id, { profileImagePath: '' });
await this.userRepository.update(auth.user.id, { profileImagePath: '' });
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
}

View file

@ -1,9 +1,10 @@
import { AuthUserDto } from '@app/domain';
import { AuthDto } from '@app/domain';
import { UserEntity } from '@app/infra/entities';
export const CLI_USER: AuthUserDto = {
id: 'cli',
email: 'cli@immich.app',
isAdmin: true,
isPublicUser: false,
isAllowUpload: true,
export const CLI_USER: AuthDto = {
user: {
id: 'cli',
email: 'cli@immich.app',
isAdmin: true,
} as UserEntity,
};

View file

@ -1,4 +1,4 @@
import { AssetResponseDto, AuthUserDto } from '@app/domain';
import { AssetResponseDto, AuthDto } from '@app/domain';
import {
Body,
Controller,
@ -16,7 +16,7 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard';
import { Auth, Authenticated, SharedLinkRoute } from '../../app.guard';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
@ -55,7 +55,7 @@ export class AssetController {
type: CreateAssetDto,
})
async uploadFile(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
@Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto,
@Response({ passthrough: true }) res: Res,
@ -73,7 +73,7 @@ export class AssetController {
sidecarFile = mapToUploadFile(_sidecarFile);
}
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile, sidecarFile);
const responseDto = await this.assetService.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
if (responseDto.duplicate) {
res.status(HttpStatus.OK);
}
@ -89,12 +89,12 @@ export class AssetController {
},
})
async serveFile(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Response() res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param() { id }: UUIDParamDto,
) {
await this.assetService.serveFile(authUser, id, query, res);
await this.assetService.serveFile(auth, id, query, res);
}
@SharedLinkRoute()
@ -106,27 +106,27 @@ export class AssetController {
},
})
async getAssetThumbnail(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Response() res: Res,
@Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
) {
await this.assetService.serveThumbnail(authUser, id, query, res);
await this.assetService.serveThumbnail(auth, id, query, res);
}
@Get('/curated-objects')
getCuratedObjects(@AuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser);
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(auth);
}
@Get('/curated-locations')
getCuratedLocations(@AuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser);
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(auth);
}
@Get('/search-terms')
getAssetSearchTerms(@AuthUser() authUser: AuthUserDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(authUser);
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(auth);
}
/**
@ -140,10 +140,10 @@ export class AssetController {
schema: { type: 'string' },
})
getAllAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
): Promise<AssetResponseDto[]> {
return this.assetService.getAllAssets(authUser, dto);
return this.assetService.getAllAssets(auth, dto);
}
/**
@ -151,8 +151,8 @@ export class AssetController {
*/
@Get('/:deviceId')
@ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' })
getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
return this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
getUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.assetService.getUserAssetsByDeviceId(auth, deviceId);
}
/**
@ -160,8 +160,8 @@ export class AssetController {
*/
@SharedLinkRoute()
@Get('/assetById/:id')
getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.assetService.getAssetById(authUser, id) as Promise<AssetResponseDto>;
getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.assetService.getAssetById(auth, id) as Promise<AssetResponseDto>;
}
/**
@ -170,10 +170,10 @@ export class AssetController {
@Post('/exist')
@HttpCode(HttpStatus.OK)
checkExistingAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Body(ValidationPipe) dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return this.assetService.checkExistingAssets(authUser, dto);
return this.assetService.checkExistingAssets(auth, dto);
}
/**
@ -182,9 +182,9 @@ export class AssetController {
@Post('/bulk-upload-check')
@HttpCode(HttpStatus.OK)
checkBulkUpload(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(authUser, dto);
return this.assetService.bulkUploadCheck(auth, dto);
}
}

View file

@ -1,4 +1,4 @@
import { AuthUserDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain';
import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain';
import { AssetEntity } from '@app/infra/entities';
import { parse } from 'node:path';
import { IAssetRepository } from './asset-repository';
@ -11,14 +11,14 @@ export class AssetCore {
) {}
async create(
authUser: AuthUserDto,
auth: AuthDto,
dto: CreateAssetDto & { libraryId: string },
file: UploadFile,
livePhotoAssetId?: string,
sidecarPath?: string,
): Promise<AssetEntity> {
const asset = await this.repository.create({
ownerId: authUser.id,
ownerId: auth.user.id,
libraryId: dto.libraryId,
checksum: file.checksum,

View file

@ -226,7 +226,7 @@ describe('AssetService', () => {
],
});
expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]);
expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
});
});
@ -235,7 +235,10 @@ describe('AssetService', () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared link access', async () => {
@ -243,7 +246,7 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id);
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
authStub.adminSharedLink.sharedLink?.id,
new Set([assetStub.image.id]),
);
});
@ -253,7 +256,7 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
authStub.admin.id,
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
@ -262,7 +265,10 @@ describe('AssetService', () => {
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(
authStub.admin.user.id,
new Set([assetStub.image.id]),
);
});
it('should throw an error for no access', async () => {

View file

@ -1,7 +1,7 @@
import {
AccessCore,
AssetResponseDto,
AuthUserDto,
AuthDto,
getLivePhotoMotionFilename,
IAccessRepository,
IJobRepository,
@ -65,7 +65,7 @@ export class AssetService {
}
public async uploadFile(
authUser: AuthUserDto,
auth: AuthDto,
dto: CreateAssetDto,
file: UploadFile,
livePhotoFile?: UploadFile,
@ -81,15 +81,15 @@ export class AssetService {
let livePhotoAsset: AssetEntity | null = null;
try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId);
const libraryId = await this.getLibraryId(auth, dto.libraryId);
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile);
}
const asset = await this.assetCore.create(
authUser,
auth,
{ ...dto, libraryId },
file,
livePhotoAsset?.id,
@ -107,7 +107,7 @@ export class AssetService {
// handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
const [duplicate] = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums);
return { id: duplicate.id, duplicate: true };
}
@ -116,33 +116,29 @@ export class AssetService {
}
}
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
public async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this._assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || authUser.id;
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId);
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this._assetRepository.getAllByUserId(userId, dto);
return assets.map((asset) => mapAsset(asset));
}
public async getAssetById(
authUser: AuthUserDto,
assetId: string,
): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
public async getAssetById(auth: AuthDto, assetId: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
const includeMetadata = this.getExifPermission(authUser);
const asset = await this._assetRepository.getById(assetId);
if (includeMetadata) {
if (!auth.sharedLink || auth.sharedLink?.showExif) {
const data = mapAsset(asset, { withStack: true });
if (data.ownerId !== authUser.id) {
if (data.ownerId !== auth.user.id) {
data.people = [];
}
if (authUser.isPublicUser) {
if (auth.sharedLink) {
delete data.owner;
}
@ -152,8 +148,8 @@ export class AssetService {
}
}
async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId);
async serveThumbnail(auth: AuthDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
const asset = await this._assetRepository.get(assetId);
if (!asset) {
@ -172,16 +168,16 @@ export class AssetService {
}
}
public async serveFile(authUser: AuthUserDto, assetId: string, query: ServeFileDto, res: Res) {
public async serveFile(auth: AuthDto, assetId: string, query: ServeFileDto, res: Res) {
// this is not quite right as sometimes this returns the original still
await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId);
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
const asset = await this._assetRepository.getById(assetId);
if (!asset) {
throw new NotFoundException('Asset does not exist');
}
const allowOriginalFile = !!(!authUser.isPublicUser || authUser.isAllowDownload);
const allowOriginalFile = !!(!auth.sharedLink || auth.sharedLink?.allowDownload);
const filepath =
asset.type === AssetType.IMAGE
@ -191,10 +187,10 @@ export class AssetService {
await this.sendFile(res, filepath);
}
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
async getAssetSearchTerm(auth: AuthDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>();
const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id);
const rows = await this._assetRepository.getSearchPropertiesByUserId(auth.user.id);
rows.forEach((row: SearchPropertiesDto) => {
// tags
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
@ -224,24 +220,24 @@ export class AssetService {
return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
}
async getCuratedLocation(authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this._assetRepository.getLocationsByUserId(authUser.id);
async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this._assetRepository.getLocationsByUserId(auth.user.id);
}
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this._assetRepository.getDetectedObjectsByUserId(authUser.id);
async getCuratedObject(auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this._assetRepository.getDetectedObjectsByUserId(auth.user.id);
}
async checkExistingAssets(
authUser: AuthUserDto,
auth: AuthDto,
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return {
existingIds: await this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto),
existingIds: await this._assetRepository.getExistingAssets(auth.user.id, checkExistingAssetsDto),
};
}
async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
// support base64 and hex checksums
for (const asset of dto.assets) {
if (asset.checksum.length === 28) {
@ -250,7 +246,7 @@ export class AssetService {
}
const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
const results = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums);
const checksumMap: Record<string, string> = {};
for (const { id, checksum } of results) {
@ -279,10 +275,6 @@ export class AssetService {
};
}
getExifPermission(authUser: AuthUserDto) {
return !authUser.isPublicUser || authUser.isShowMetadata;
}
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
switch (format) {
case GetAssetThumbnailFormatEnum.WEBP:
@ -354,15 +346,15 @@ export class AssetService {
}
}
private async getLibraryId(authUser: AuthUserDto, libraryId?: string) {
private async getLibraryId(auth: AuthDto, libraryId?: string) {
if (libraryId) {
return libraryId;
}
let library = await this.libraryRepository.getDefaultUploadLibrary(authUser.id);
let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id);
if (!library) {
library = await this.libraryRepository.create({
ownerId: authUser.id,
ownerId: auth.user.id,
name: 'Default Library',
assets: [],
type: LibraryType.UPLOAD,

View file

@ -1,4 +1,4 @@
import { AuthService, AuthUserDto, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain';
import { AuthDto, AuthService, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain';
import {
CanActivate,
ExecutionContext,
@ -50,8 +50,8 @@ export const SharedLinkRoute = () =>
applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false }));
export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value);
export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user;
export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => {
return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user;
});
export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
@ -67,7 +67,7 @@ export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext
});
export interface AuthRequest extends Request {
user?: AuthUserDto;
user?: AuthDto;
}
@Injectable()
@ -93,12 +93,12 @@ export class AppGuard implements CanActivate {
const req = context.switchToHttp().getRequest<AuthRequest>();
const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
if (authDto.isPublicUser && !isSharedRoute) {
if (authDto.sharedLink && !isSharedRoute) {
this.logger.warn(`Denied access to non-shared route: ${req.path}`);
return false;
}
if (isAdminRoute && !authDto.isAdmin) {
if (isAdminRoute && !authDto.user.isAdmin) {
this.logger.warn(`Denied access to admin only route: ${req.path}`);
return false;
}

View file

@ -1,4 +1,4 @@
import { AuthUserDto } from '@app/domain';
import { AuthDto } from '@app/domain';
import {
ActivityDto,
ActivitySearchDto,
@ -10,7 +10,7 @@ import {
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -22,22 +22,22 @@ export class ActivityController {
constructor(private service: ActivityService) {}
@Get()
getActivities(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivitySearchDto): Promise<ResponseDto[]> {
return this.service.getAll(authUser, dto);
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ResponseDto[]> {
return this.service.getAll(auth, dto);
}
@Get('statistics')
getActivityStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivityDto): Promise<StatsResponseDto> {
return this.service.getStatistics(authUser, dto);
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<StatsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Post()
async createActivity(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Body() dto: CreateDto,
@Res({ passthrough: true }) res: Response,
): Promise<ResponseDto> {
const { duplicate, value } = await this.service.create(authUser, dto);
const { duplicate, value } = await this.service.create(auth, dto);
if (duplicate) {
res.status(HttpStatus.OK);
}
@ -46,7 +46,7 @@ export class ActivityController {
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteActivity(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}

View file

@ -4,7 +4,7 @@ import {
AlbumInfoDto,
AlbumResponseDto,
AlbumService,
AuthUserDto,
AuthDto,
BulkIdResponseDto,
BulkIdsDto,
CreateAlbumDto as CreateDto,
@ -14,7 +14,7 @@ import {
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -26,78 +26,78 @@ export class AlbumController {
constructor(private service: AlbumService) {}
@Get('count')
getAlbumCount(@AuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(authUser);
getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(auth);
}
@Get()
getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(authUser, query);
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query);
}
@Post()
createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise<AlbumResponseDto> {
return this.service.create(authUser, dto);
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise<AlbumResponseDto> {
return this.service.create(auth, dto);
}
@SharedLinkRoute()
@Get(':id')
getAlbumInfo(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Query() dto: AlbumInfoDto,
): Promise<AlbumResponseDto> {
return this.service.get(authUser, id, dto);
return this.service.get(auth, id, dto);
}
@Patch(':id')
updateAlbumInfo(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<AlbumResponseDto> {
return this.service.update(authUser, id, dto);
return this.service.update(auth, id, dto);
}
@Delete(':id')
deleteAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(authUser, id);
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id);
}
@SharedLinkRoute()
@Put(':id/assets')
addAssetsToAlbum(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: BulkIdsDto,
): Promise<BulkIdResponseDto[]> {
return this.service.addAssets(authUser, id, dto);
return this.service.addAssets(auth, id, dto);
}
@Delete(':id/assets')
removeAssetFromAlbum(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Body() dto: BulkIdsDto,
@Param() { id }: UUIDParamDto,
): Promise<BulkIdResponseDto[]> {
return this.service.removeAssets(authUser, id, dto);
return this.service.removeAssets(auth, id, dto);
}
@Put(':id/users')
addUsersToAlbum(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AddUsersDto,
): Promise<AlbumResponseDto> {
return this.service.addUsers(authUser, id, dto);
return this.service.addUsers(auth, id, dto);
}
@Delete(':id/user/:userId')
removeUserFromAlbum(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
) {
return this.service.removeUser(authUser, id, userId);
return this.service.removeUser(auth, id, userId);
}
}

View file

@ -4,11 +4,11 @@ import {
APIKeyResponseDto,
APIKeyService,
APIKeyUpdateDto,
AuthUserDto,
AuthDto,
} from '@app/domain';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -20,31 +20,31 @@ export class APIKeyController {
constructor(private service: APIKeyService) {}
@Post()
createApiKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto);
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(auth, dto);
}
@Get()
getApiKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser);
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(auth);
}
@Get(':id')
getApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(auth, id);
}
@Put(':id')
updateApiKey(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: APIKeyUpdateDto,
): Promise<APIKeyResponseDto> {
return this.service.update(authUser, id, dto);
return this.service.update(auth, id, dto);
}
@Delete(':id')
deleteApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}

View file

@ -8,7 +8,7 @@ import {
AssetService,
AssetStatsDto,
AssetStatsResponseDto,
AuthUserDto,
AuthDto,
BulkIdsDto,
DownloadInfoDto,
DownloadResponseDto,
@ -39,7 +39,7 @@ import {
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation, asStreamableFile } from '../app.utils';
import { Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -52,8 +52,8 @@ export class AssetsController {
constructor(private service: AssetService) {}
@Get()
searchAssets(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
return this.service.search(authUser, dto);
searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
return this.service.search(auth, dto);
}
}
@ -65,115 +65,111 @@ export class AssetController {
constructor(private service: AssetService) {}
@Get('map-marker')
getMapMarkers(@AuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(authUser, options);
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(auth, options);
}
@Get('memory-lane')
getMemoryLane(@AuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(authUser, dto);
getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(auth, dto);
}
@Get('random')
getRandom(@AuthUser() authUser: AuthUserDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(authUser, dto.count ?? 1);
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(auth, dto.count ?? 1);
}
@SharedLinkRoute()
@Post('download/info')
getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(authUser, dto);
getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(auth, dto);
}
@SharedLinkRoute()
@Post('download/archive')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(authUser, dto).then(asStreamableFile);
downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(auth, dto).then(asStreamableFile);
}
@SharedLinkRoute()
@Post('download/:id')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.downloadFile(authUser, id).then(asStreamableFile);
downloadFile(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.downloadFile(auth, id).then(asStreamableFile);
}
/**
* Get all asset of a device that are in the database, ID only.
*/
@Get('/device/:deviceId')
getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(authUser, deviceId);
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId);
}
@Get('statistics')
getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(authUser, dto);
getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Authenticated({ isShared: true })
@Get('time-buckets')
getTimeBuckets(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(authUser, dto);
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(auth, dto);
}
@Authenticated({ isShared: true })
@Get('time-bucket')
getTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
}
@Post('jobs')
@HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(authUser, dto);
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(auth, dto);
}
@Put()
@HttpCode(HttpStatus.NO_CONTENT)
updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(authUser, dto);
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto);
}
@Delete()
@HttpCode(HttpStatus.NO_CONTENT)
deleteAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(authUser, dto);
deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(auth, dto);
}
@Post('restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAll(authUser, dto);
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAll(auth, dto);
}
@Post('trash/empty')
@HttpCode(HttpStatus.NO_CONTENT)
emptyTrash(@AuthUser() authUser: AuthUserDto): Promise<void> {
return this.service.handleTrashAction(authUser, TrashAction.EMPTY_ALL);
emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(auth, TrashAction.EMPTY_ALL);
}
@Post('trash/restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@AuthUser() authUser: AuthUserDto): Promise<void> {
return this.service.handleTrashAction(authUser, TrashAction.RESTORE_ALL);
restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(auth, TrashAction.RESTORE_ALL);
}
@Put('stack/parent')
@HttpCode(HttpStatus.OK)
updateStackParent(@AuthUser() authUser: AuthUserDto, @Body() dto: UpdateStackParentDto): Promise<void> {
return this.service.updateStackParent(authUser, dto);
updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> {
return this.service.updateStackParent(auth, dto);
}
@Put(':id')
updateAsset(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<AssetResponseDto> {
return this.service.update(authUser, id, dto);
updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<AssetResponseDto> {
return this.service.update(auth, id, dto);
}
}

View file

@ -2,7 +2,7 @@ import {
AuditDeletesDto,
AuditDeletesResponseDto,
AuditService,
AuthUserDto,
AuthDto,
FileChecksumDto,
FileChecksumResponseDto,
FileReportDto,
@ -10,7 +10,7 @@ import {
} from '@app/domain';
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
import { AdminRoute, Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
@ApiTags('Audit')
@ -21,8 +21,8 @@ export class AuditController {
constructor(private service: AuditService) {}
@Get('deletes')
getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(authUser, dto);
getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(auth, dto);
}
@AdminRoute()

View file

@ -1,7 +1,7 @@
import {
AuthDeviceResponseDto,
AuthDto,
AuthService,
AuthUserDto,
ChangePasswordDto,
IMMICH_ACCESS_COOKIE,
IMMICH_AUTH_TYPE_COOKIE,
@ -17,7 +17,7 @@ import {
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -47,20 +47,20 @@ export class AuthController {
}
@Get('devices')
getAuthDevices(@AuthUser() authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
return this.service.getDevices(authUser);
getAuthDevices(@Auth() auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
return this.service.getDevices(auth);
}
@Delete('devices')
@HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevices(@AuthUser() authUser: AuthUserDto): Promise<void> {
return this.service.logoutDevices(authUser);
logoutAuthDevices(@Auth() auth: AuthDto): Promise<void> {
return this.service.logoutDevices(auth);
}
@Delete('devices/:id')
@HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevice(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.logoutDevice(authUser, id);
logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.logoutDevice(auth, id);
}
@Post('validateToken')
@ -71,8 +71,8 @@ export class AuthController {
@Post('change-password')
@HttpCode(HttpStatus.OK)
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(authUser, dto).then(mapUser);
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(auth, dto).then(mapUser);
}
@Post('logout')
@ -80,11 +80,11 @@ export class AuthController {
logout(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
): Promise<LogoutResponseDto> {
res.clearCookie(IMMICH_ACCESS_COOKIE);
res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
return this.service.logout(authUser, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
return this.service.logout(auth, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
}
}

View file

@ -1,7 +1,7 @@
import { AssetFaceResponseDto, AuthUserDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain';
import { AssetFaceResponseDto, AuthDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain';
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -13,16 +13,16 @@ export class FaceController {
constructor(private service: PersonService) {}
@Get()
getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(authUser, dto);
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(auth, dto);
}
@Put(':id')
reassignFacesById(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: FaceDto,
): Promise<PersonResponseDto> {
return this.service.reassignFacesById(authUser, id, dto);
return this.service.reassignFacesById(auth, id, dto);
}
}

View file

@ -1,5 +1,5 @@
import {
AuthUserDto,
AuthDto,
CreateLibraryDto as CreateDto,
LibraryService,
LibraryStatsResponseDto,
@ -9,7 +9,7 @@ import {
} from '@app/domain';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -21,49 +21,42 @@ export class LibraryController {
constructor(private service: LibraryService) {}
@Get()
getLibraries(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> {
return this.service.getAllForUser(authUser);
getLibraries(@Auth() auth: AuthDto): Promise<ResponseDto[]> {
return this.service.getAllForUser(auth);
}
@Post()
createLibrary(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise<ResponseDto> {
return this.service.create(authUser, dto);
createLibrary(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise<ResponseDto> {
return this.service.create(auth, dto);
}
@Put(':id')
updateLibrary(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<ResponseDto> {
return this.service.update(authUser, id, dto);
updateLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<ResponseDto> {
return this.service.update(auth, id, dto);
}
@Get(':id')
getLibraryInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
return this.service.get(authUser, id);
getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
return this.service.get(auth, id);
}
@Delete(':id')
deleteLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
deleteLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
@Get(':id/statistics')
getLibraryStatistics(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(authUser, id);
getLibraryStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(auth, id);
}
@Post(':id/scan')
scanLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(authUser, id, dto);
scanLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(auth, id, dto);
}
@Post(':id/removeOffline')
removeOfflineFiles(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(authUser, id);
removeOfflineFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(auth, id);
}
}

View file

@ -1,6 +1,6 @@
import {
AuthDto,
AuthService,
AuthUserDto,
LoginDetails,
LoginResponseDto,
OAuthAuthorizeResponseDto,
@ -12,7 +12,7 @@ import {
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
@ApiTags('OAuth')
@ -58,12 +58,12 @@ export class OAuthController {
}
@Post('link')
linkOAuthAccount(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(authUser, dto);
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(auth, dto);
}
@Post('unlink')
unlinkOAuthAccount(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return this.service.unlink(authUser);
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.unlink(auth);
}
}

View file

@ -1,8 +1,8 @@
import { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain';
import { AuthDto, PartnerDirection, PartnerService } from '@app/domain';
import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -15,29 +15,26 @@ export class PartnerController {
@Get()
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
getPartners(
@AuthUser() authUser: AuthUserDto,
@Query('direction') direction: PartnerDirection,
): Promise<PartnerResponseDto[]> {
return this.service.getAll(authUser, direction);
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
return this.service.getAll(auth, direction);
}
@Post(':id')
createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(authUser, id);
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(auth, id);
}
@Put(':id')
updatePartner(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdatePartnerDto,
): Promise<PartnerResponseDto> {
return this.service.update(authUser, id, dto);
return this.service.update(auth, id, dto);
}
@Delete(':id')
removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
}

View file

@ -1,7 +1,7 @@
import {
AssetFaceUpdateDto,
AssetResponseDto,
AuthUserDto,
AuthDto,
BulkIdResponseDto,
ImmichReadStream,
MergePersonDto,
@ -15,7 +15,7 @@ import {
} from '@app/domain';
import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -31,49 +31,46 @@ export class PersonController {
constructor(private service: PersonService) {}
@Get()
getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(authUser, withHidden);
getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, withHidden);
}
@Post()
createPerson(@AuthUser() authUser: AuthUserDto): Promise<PersonResponseDto> {
return this.service.createPerson(authUser);
createPerson(@Auth() auth: AuthDto): Promise<PersonResponseDto> {
return this.service.createPerson(auth);
}
@Put(':id/reassign')
reassignFaces(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetFaceUpdateDto,
): Promise<PersonResponseDto[]> {
return this.service.reassignFaces(authUser, id, dto);
return this.service.reassignFaces(auth, id, dto);
}
@Put()
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updatePeople(authUser, dto);
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updatePeople(auth, dto);
}
@Get(':id')
getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(authUser, id);
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(auth, id);
}
@Put(':id')
updatePerson(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: PersonUpdateDto,
): Promise<PersonResponseDto> {
return this.service.update(authUser, id, dto);
return this.service.update(auth, id, dto);
}
@Get(':id/statistics')
getPersonStatistics(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(authUser, id);
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(auth, id);
}
@Get(':id/thumbnail')
@ -82,21 +79,21 @@ export class PersonController {
'image/jpeg': { schema: { type: 'string', format: 'binary' } },
},
})
getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.getThumbnail(authUser, id).then(asStreamableFile);
getPersonThumbnail(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.getThumbnail(auth, id).then(asStreamableFile);
}
@Get(':id/assets')
getPersonAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(authUser, id);
getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id);
}
@Post(':id/merge')
mergePerson(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: MergePersonDto,
): Promise<BulkIdResponseDto[]> {
return this.service.mergePerson(authUser, id, dto);
return this.service.mergePerson(auth, id, dto);
}
}

View file

@ -1,5 +1,5 @@
import {
AuthUserDto,
AuthDto,
PersonResponseDto,
SearchDto,
SearchExploreResponseDto,
@ -9,7 +9,7 @@ import {
} from '@app/domain';
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
@ApiTags('Search')
@ -20,17 +20,17 @@ export class SearchController {
constructor(private service: SearchService) {}
@Get()
search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(authUser, dto);
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(auth, dto);
}
@Get('explore')
getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>;
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
}
@Get('person')
searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(authUser, dto);
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(auth, dto);
}
}

View file

@ -1,7 +1,7 @@
import {
AssetIdsDto,
AssetIdsResponseDto,
AuthUserDto,
AuthDto,
IMMICH_SHARED_LINK_ACCESS_COOKIE,
SharedLinkCreateDto,
SharedLinkEditDto,
@ -12,7 +12,7 @@ import {
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -24,14 +24,14 @@ export class SharedLinkController {
constructor(private readonly service: SharedLinkService) {}
@Get()
getAllSharedLinks(@AuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(authUser);
getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth);
}
@SharedLinkRoute()
@Get('me')
async getMySharedLink(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Query() dto: SharedLinkPasswordDto,
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
@ -40,58 +40,58 @@ export class SharedLinkController {
if (sharedLinkToken) {
dto.token = sharedLinkToken;
}
const sharedLinkResponse = await this.service.getMine(authUser, dto);
if (sharedLinkResponse.token) {
res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, sharedLinkResponse.token, {
const response = await this.service.getMine(auth, dto);
if (response.token) {
res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
httpOnly: true,
sameSite: 'lax',
});
}
return sharedLinkResponse;
return response;
}
@Get(':id')
getSharedLinkById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(authUser, id);
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(auth, id);
}
@Post()
createSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(authUser, dto);
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(auth, dto);
}
@Patch(':id')
updateSharedLink(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: SharedLinkEditDto,
): Promise<SharedLinkResponseDto> {
return this.service.update(authUser, id, dto);
return this.service.update(auth, id, dto);
}
@Delete(':id')
removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
@SharedLinkRoute()
@Put(':id/assets')
addSharedLinkAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto);
return this.service.addAssets(auth, id, dto);
}
@SharedLinkRoute()
@Delete(':id/assets')
removeSharedLinkAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto);
return this.service.removeAssets(auth, id, dto);
}
}

View file

@ -2,7 +2,7 @@ import {
AssetIdsDto,
AssetIdsResponseDto,
AssetResponseDto,
AuthUserDto,
AuthDto,
CreateTagDto,
TagResponseDto,
TagService,
@ -10,7 +10,7 @@ import {
} from '@app/domain';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -22,54 +22,50 @@ export class TagController {
constructor(private service: TagService) {}
@Post()
createTag(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(authUser, dto);
createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(auth, dto);
}
@Get()
getAllTags(@AuthUser() authUser: AuthUserDto): Promise<TagResponseDto[]> {
return this.service.getAll(authUser);
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(auth);
}
@Get(':id')
getTagById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(authUser, id);
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(auth, id);
}
@Patch(':id')
updateTag(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateTagDto,
): Promise<TagResponseDto> {
return this.service.update(authUser, id, dto);
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
deleteTag(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
@Get(':id/assets')
getTagAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(authUser, id);
getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id);
}
@Put(':id/assets')
tagAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto);
return this.service.addAssets(auth, id, dto);
}
@Delete(':id/assets')
untagAssets(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@Body() dto: AssetIdsDto,
@Param() { id }: UUIDParamDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto);
return this.service.removeAssets(auth, id, dto);
}
}

View file

@ -1,5 +1,5 @@
import {
AuthUserDto,
AuthDto,
CreateUserDto as CreateDto,
CreateProfileImageDto,
CreateProfileImageResponseDto,
@ -23,7 +23,7 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
import { AdminRoute, Auth, Authenticated } from '../app.guard';
import { UseValidation, asStreamableFile } from '../app.utils';
import { FileUploadInterceptor, Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -36,8 +36,8 @@ export class UserController {
constructor(private service: UserService) {}
@Get()
getAllUsers(@AuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.getAll(authUser, isAll);
getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.getAll(auth, isAll);
}
@Get('info/:id')
@ -46,8 +46,8 @@ export class UserController {
}
@Get('me')
getMyUserInfo(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return this.service.getMe(authUser);
getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.getMe(auth);
}
@AdminRoute()
@ -58,26 +58,26 @@ export class UserController {
@Delete('profile-image')
@HttpCode(HttpStatus.NO_CONTENT)
deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise<void> {
return this.service.deleteProfileImage(authUser);
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth);
}
@AdminRoute()
@Delete(':id')
deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.delete(authUser, id);
deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.delete(auth, id);
}
@AdminRoute()
@Post(':id/restore')
restoreUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(authUser, id);
restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(auth, id);
}
// TODO: replace with @Put(':id')
@Put()
updateUser(@AuthUser() authUser: AuthUserDto, @Body() updateUserDto: UpdateDto): Promise<UserResponseDto> {
return this.service.update(authUser, updateUserDto);
updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateDto): Promise<UserResponseDto> {
return this.service.update(auth, updateUserDto);
}
@UseInterceptors(FileUploadInterceptor)
@ -85,10 +85,10 @@ export class UserController {
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@Post('profile-image')
createProfileImage(
@AuthUser() authUser: AuthUserDto,
@Auth() auth: AuthDto,
@UploadedFile() fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> {
return this.service.createProfileImage(authUser, fileInfo);
return this.service.createProfileImage(auth, fileInfo);
}
@Get('profile-image/:id')

View file

@ -44,7 +44,7 @@ const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>)
const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
return {
authUser: req.user || null,
auth: req.user || null,
fieldName: file.fieldname as UploadFieldName,
file: mapToUploadFile(file as ImmichFile),
};

View file

@ -19,10 +19,10 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi
async handleConnection(client: Socket) {
try {
this.logger.log(`Websocket Connect: ${client.id}`);
const user = await this.authService.validate(client.request.headers, {});
await client.join(user.id);
const auth = await this.authService.validate(client.request.headers, {});
await client.join(auth.user.id);
for (const callback of this.onConnectCallbacks) {
await callback(user.id);
await callback(auth.user.id);
}
} catch (error: Error | any) {
this.logger.error(`Websocket connection error: ${error}`, error?.stack);

View file

@ -9,7 +9,7 @@ export const activityStub = {
id: 'activity-1',
comment: 'comment',
isLiked: false,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
assetId: assetStub.image.id,
asset: assetStub.image,
@ -22,7 +22,7 @@ export const activityStub = {
id: 'activity-2',
comment: null,
isLiked: true,
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
assetId: assetStub.image.id,
asset: assetStub.image,

View file

@ -8,7 +8,7 @@ export const albumStub = {
id: 'album-1',
albumName: 'Empty album',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
@ -24,7 +24,7 @@ export const albumStub = {
id: 'album-2',
albumName: 'Empty album shared with user',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
@ -40,7 +40,7 @@ export const albumStub = {
id: 'album-3',
albumName: 'Empty album shared with users',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
@ -56,7 +56,7 @@ export const albumStub = {
id: 'album-3',
albumName: 'Empty album shared with admin',
description: '',
ownerId: authStub.user1.id,
ownerId: authStub.user1.user.id,
owner: userStub.user1,
assets: [],
albumThumbnailAsset: null,
@ -72,7 +72,7 @@ export const albumStub = {
id: 'album-4',
albumName: 'Album with one asset',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [assetStub.image],
albumThumbnailAsset: null,
@ -88,7 +88,7 @@ export const albumStub = {
id: 'album-4a',
albumName: 'Album with two assets',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [assetStub.image, assetStub.withLocation],
albumThumbnailAsset: assetStub.image,
@ -104,7 +104,7 @@ export const albumStub = {
id: 'album-5',
albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: assetStub.image,
@ -120,7 +120,7 @@ export const albumStub = {
id: 'album-5',
albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
@ -136,7 +136,7 @@ export const albumStub = {
id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [assetStub.image],
albumThumbnailAsset: assetStub.livePhotoMotionAsset,
@ -152,7 +152,7 @@ export const albumStub = {
id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [assetStub.image],
albumThumbnailAsset: assetStub.image,

View file

@ -7,7 +7,7 @@ export const keyStub = {
id: 'my-random-guid',
name: 'My Key',
key: 'my-api-key (hashed)',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
} as APIKeyEntity),
};

View file

@ -403,7 +403,7 @@ export const assetStub = {
livePhotoMotionAsset: Object.freeze({
id: 'live-photo-motion-asset',
originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.id,
ownerId: authStub.user1.user.id,
type: AssetType.VIDEO,
isVisible: false,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
@ -418,7 +418,7 @@ export const assetStub = {
livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset',
originalPath: fileStub.livePhotoStill.originalPath,
ownerId: authStub.user1.id,
ownerId: authStub.user1.user.id,
type: AssetType.IMAGE,
livePhotoVideoId: 'live-photo-motion-asset',
isVisible: true,

View file

@ -7,7 +7,7 @@ export const auditStub = {
entityId: 'asset-created',
action: DatabaseAction.CREATE,
entityType: EntityType.ASSET,
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
createdAt: new Date(),
}),
update: Object.freeze<AuditEntity>({
@ -15,7 +15,7 @@ export const auditStub = {
entityId: 'asset-updated',
action: DatabaseAction.UPDATE,
entityType: EntityType.ASSET,
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
createdAt: new Date(),
}),
delete: Object.freeze<AuditEntity>({
@ -23,7 +23,7 @@ export const auditStub = {
entityId: 'asset-deleted',
action: DatabaseAction.DELETE,
entityType: EntityType.ASSET,
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
createdAt: new Date(),
}),
};

View file

@ -1,4 +1,5 @@
import { AuthUserDto } from '@app/domain';
import { AuthDto } from '@app/domain';
import { SharedLinkEntity, UserEntity, UserTokenEntity } from '../../src/infra/entities';
export const adminSignupStub = {
name: 'Immich Admin',
@ -24,77 +25,84 @@ export const changePasswordStub = {
};
export const authStub = {
admin: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
isPublicUser: false,
isAllowUpload: true,
externalPath: null,
admin: Object.freeze<AuthDto>({
user: {
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
} as UserEntity,
}),
user1: Object.freeze<AuthUserDto>({
id: 'user-id',
email: 'immich@test.com',
isAdmin: false,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowMetadata: true,
accessTokenId: 'token-id',
externalPath: null,
user1: Object.freeze<AuthDto>({
user: {
id: 'user-id',
email: 'immich@test.com',
isAdmin: false,
} as UserEntity,
userToken: {
id: 'token-id',
} as UserTokenEntity,
}),
user2: Object.freeze<AuthUserDto>({
id: 'user-2',
email: 'user2@immich.app',
isAdmin: false,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowMetadata: true,
accessTokenId: 'token-id',
externalPath: null,
user2: Object.freeze<AuthDto>({
user: {
id: 'user-2',
email: 'user2@immich.app',
isAdmin: false,
} as UserEntity,
userToken: {
id: 'token-id',
} as UserTokenEntity,
}),
external1: Object.freeze<AuthUserDto>({
id: 'user-id',
email: 'immich@test.com',
isAdmin: false,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowMetadata: true,
accessTokenId: 'token-id',
externalPath: '/data/user1',
external1: Object.freeze<AuthDto>({
user: {
id: 'user-id',
email: 'immich@test.com',
isAdmin: false,
externalPath: '/data/user1',
} as UserEntity,
userToken: {
id: 'token-id',
} as UserTokenEntity,
}),
adminSharedLink: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
isAllowUpload: true,
isAllowDownload: true,
isPublicUser: true,
isShowMetadata: true,
sharedLinkId: '123',
adminSharedLink: Object.freeze<AuthDto>({
user: {
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
} as UserEntity,
sharedLink: {
id: '123',
showExif: true,
allowDownload: true,
allowUpload: true,
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}),
adminSharedLinkNoExif: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
isAllowUpload: true,
isAllowDownload: true,
isPublicUser: true,
isShowMetadata: false,
sharedLinkId: '123',
adminSharedLinkNoExif: Object.freeze<AuthDto>({
user: {
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
} as UserEntity,
sharedLink: {
id: '123',
showExif: false,
allowDownload: true,
allowUpload: true,
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}),
readonlySharedLink: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
isAllowUpload: false,
isAllowDownload: false,
isPublicUser: true,
isShowMetadata: true,
sharedLinkId: '123',
accessTokenId: 'token-id',
readonlySharedLink: Object.freeze<AuthDto>({
user: {
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
} as UserEntity,
sharedLink: {
id: '123',
allowUpload: false,
allowDownload: false,
showExif: true,
} as SharedLinkEntity,
}),
};

View file

@ -106,7 +106,7 @@ const albumResponse: AlbumResponseDto = {
export const sharedLinkStub = {
individual: Object.freeze({
id: '123',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
key: sharedLinkBytes,
type: SharedLinkType.INDIVIDUAL,
@ -121,7 +121,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity),
valid: Object.freeze({
id: '123',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
key: sharedLinkBytes,
type: SharedLinkType.ALBUM,
@ -138,7 +138,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity),
expired: Object.freeze({
id: '123',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
key: sharedLinkBytes,
type: SharedLinkType.ALBUM,
@ -154,7 +154,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity),
readonlyNoExif: Object.freeze<SharedLinkEntity>({
id: '123',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
key: sharedLinkBytes,
type: SharedLinkType.ALBUM,
@ -169,7 +169,7 @@ export const sharedLinkStub = {
albumId: 'album-123',
album: {
id: 'album-123',
ownerId: authStub.admin.id,
ownerId: authStub.admin.user.id,
owner: userStub.admin,
albumName: 'Test Album',
description: '',
@ -260,7 +260,7 @@ export const sharedLinkStub = {
}),
passwordRequired: Object.freeze<SharedLinkEntity>({
id: '123',
userId: authStub.admin.id,
userId: authStub.admin.user.id,
user: userStub.admin,
key: sharedLinkBytes,
type: SharedLinkType.ALBUM,

View file

@ -21,7 +21,7 @@ export const userDto = {
export const userStub = {
admin: Object.freeze<UserEntity>({
...authStub.admin,
...authStub.admin.user,
password: 'admin_password',
name: 'admin_name',
storageLabel: 'admin',
@ -38,7 +38,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
user1: Object.freeze<UserEntity>({
...authStub.user1,
...authStub.user1.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: null,
@ -55,7 +55,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
user2: Object.freeze<UserEntity>({
...authStub.user2,
...authStub.user2.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: null,
@ -72,7 +72,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
storageLabel: Object.freeze<UserEntity>({
...authStub.user1,
...authStub.user1.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',
@ -89,7 +89,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
externalPath1: Object.freeze<UserEntity>({
...authStub.user1,
...authStub.user1.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',
@ -106,7 +106,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
externalPath2: Object.freeze<UserEntity>({
...authStub.user1,
...authStub.user1.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',
@ -123,7 +123,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY,
}),
profilePath: Object.freeze<UserEntity>({
...authStub.user1,
...authStub.user1.user,
password: 'immich_password',
name: 'immich_name',
storageLabel: 'label-1',