2023-12-14 11:55:40 -05:00
|
|
|
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
2024-02-08 16:56:19 -05:00
|
|
|
import { DateTime } from 'luxon';
|
2024-03-20 21:20:38 +01:00
|
|
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
|
|
|
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
|
|
|
import { UserCore } from 'src/cores/user.core';
|
2024-03-20 23:53:07 +01:00
|
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
|
|
|
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
|
|
|
|
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
2024-05-22 08:13:36 -04:00
|
|
|
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
2024-03-20 16:02:51 -05:00
|
|
|
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|
|
|
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
|
|
|
|
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
|
|
|
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
2024-04-17 03:00:31 +05:30
|
|
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
2024-05-15 18:58:23 -04:00
|
|
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
2024-03-20 22:15:09 -05:00
|
|
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
2024-05-22 08:13:36 -04:00
|
|
|
import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
|
2022-02-03 10:06:44 -06:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class UserService {
|
2024-03-06 00:45:40 -05:00
|
|
|
private configCore: SystemConfigCore;
|
2022-12-23 21:08:50 +01:00
|
|
|
private userCore: UserCore;
|
2023-03-25 10:50:57 -04:00
|
|
|
|
2023-01-27 20:50:07 +00:00
|
|
|
constructor(
|
2023-02-25 09:12:03 -05:00
|
|
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
2024-03-23 14:33:25 -04:00
|
|
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
2023-02-25 09:12:03 -05:00
|
|
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
2023-10-11 04:14:44 +02:00
|
|
|
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
2023-02-25 09:12:03 -05:00
|
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
2024-05-15 18:58:23 -04:00
|
|
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
2023-10-11 04:14:44 +02:00
|
|
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
2024-04-17 03:00:31 +05:30
|
|
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
2023-01-27 20:50:07 +00:00
|
|
|
) {
|
2023-10-23 14:38:48 +02:00
|
|
|
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
|
2024-04-17 03:00:31 +05:30
|
|
|
this.logger.setContext(UserService.name);
|
2024-05-15 18:58:23 -04:00
|
|
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
2022-12-23 21:08:50 +01:00
|
|
|
}
|
2022-05-21 02:23:55 -05:00
|
|
|
|
2024-04-17 08:27:04 -04:00
|
|
|
async listUsers(): Promise<UserResponseDto[]> {
|
|
|
|
|
const users = await this.userRepository.getList({ withDeleted: true });
|
|
|
|
|
return users.map((user) => mapUser(user));
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
|
2023-10-30 17:02:36 -04:00
|
|
|
const users = await this.userRepository.getList({ withDeleted: !isAll });
|
2024-02-02 04:18:00 +01:00
|
|
|
return users.map((user) => mapUser(user));
|
2022-04-23 21:08:45 -05:00
|
|
|
}
|
2022-05-21 02:23:55 -05:00
|
|
|
|
2023-10-30 19:38:34 -04:00
|
|
|
async get(userId: string): Promise<UserResponseDto> {
|
2023-10-31 11:01:32 -04:00
|
|
|
const user = await this.userRepository.get(userId, { withDeleted: false });
|
2022-07-16 23:52:00 -05:00
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mapUser(user);
|
|
|
|
|
}
|
2022-07-17 15:09:26 -05:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
getMe(auth: AuthDto): Promise<UserResponseDto> {
|
|
|
|
|
return this.findOrFail(auth.user.id, {}).then(mapUser);
|
2022-06-27 15:13:07 -05:00
|
|
|
}
|
|
|
|
|
|
2024-05-02 16:43:18 +02:00
|
|
|
async create(dto: CreateUserDto): Promise<UserResponseDto> {
|
2024-05-22 08:13:36 -04:00
|
|
|
const { memoriesEnabled, notify, ...rest } = dto;
|
|
|
|
|
let user = await this.userCore.createUser(rest);
|
|
|
|
|
|
|
|
|
|
// TODO remove and replace with entire dto.preferences config
|
|
|
|
|
if (memoriesEnabled === false) {
|
|
|
|
|
await this.userRepository.upsertMetadata(user.id, {
|
|
|
|
|
key: UserMetadataKey.PREFERENCES,
|
|
|
|
|
value: { memories: { enabled: false } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
user = await this.findOrFail(user.id, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
|
|
|
|
|
if (notify) {
|
2024-05-02 16:43:18 +02:00
|
|
|
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id: user.id, tempPassword } });
|
|
|
|
|
}
|
|
|
|
|
return mapUser(user);
|
2022-05-21 02:23:55 -05:00
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
|
2024-01-15 09:04:29 -06:00
|
|
|
const user = await this.findOrFail(dto.id, {});
|
|
|
|
|
|
|
|
|
|
if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) {
|
|
|
|
|
await this.userRepository.syncUsage(dto.id);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-22 08:13:36 -04:00
|
|
|
// TODO replace with entire preferences object
|
|
|
|
|
if (dto.memoriesEnabled !== undefined || dto.avatarColor) {
|
|
|
|
|
const newPreferences = getPreferences(user);
|
|
|
|
|
if (dto.memoriesEnabled !== undefined) {
|
|
|
|
|
newPreferences.memories.enabled = dto.memoriesEnabled;
|
|
|
|
|
delete dto.memoriesEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dto.avatarColor) {
|
|
|
|
|
newPreferences.avatar.color = dto.avatarColor;
|
|
|
|
|
delete dto.avatarColor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.userRepository.upsertMetadata(dto.id, {
|
|
|
|
|
key: UserMetadataKey.PREFERENCES,
|
|
|
|
|
value: getPreferencesPartial(user, newPreferences),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = await this.userCore.updateUser(auth.user, dto.id, dto);
|
|
|
|
|
|
|
|
|
|
return mapUser(updatedUser);
|
2022-05-21 02:23:55 -05:00
|
|
|
}
|
2022-05-27 22:15:35 -05:00
|
|
|
|
2024-03-08 17:49:39 -05:00
|
|
|
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
|
|
|
|
|
const { force } = dto;
|
|
|
|
|
const { isAdmin } = await this.findOrFail(id, {});
|
|
|
|
|
if (isAdmin) {
|
2023-10-31 11:01:32 -04:00
|
|
|
throw new ForbiddenException('Cannot delete admin user');
|
2022-11-07 16:53:47 -05:00
|
|
|
}
|
2023-10-31 11:01:32 -04:00
|
|
|
|
|
|
|
|
await this.albumRepository.softDeleteAll(id);
|
|
|
|
|
|
2024-03-08 17:49:39 -05:00
|
|
|
const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
|
|
|
|
|
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
|
|
|
|
|
|
|
|
|
if (force) {
|
|
|
|
|
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mapUser(user);
|
2022-11-07 16:53:47 -05:00
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
|
2024-03-08 17:49:39 -05:00
|
|
|
await this.findOrFail(id, { withDeleted: true });
|
2023-10-31 11:01:32 -04:00
|
|
|
await this.albumRepository.restoreAll(id);
|
2024-03-08 17:49:39 -05:00
|
|
|
return this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }).then(mapUser);
|
2022-11-07 16:53:47 -05:00
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
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 });
|
2023-11-14 04:10:35 +01:00
|
|
|
if (oldpath !== '') {
|
|
|
|
|
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
|
|
|
|
|
}
|
2022-12-23 21:08:50 +01:00
|
|
|
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
|
2022-05-27 22:24:58 -05:00
|
|
|
}
|
2022-05-27 22:15:35 -05:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
async deleteProfileImage(auth: AuthDto): Promise<void> {
|
|
|
|
|
const user = await this.findOrFail(auth.user.id, { withDeleted: false });
|
2023-11-14 04:10:35 +01:00
|
|
|
if (user.profileImagePath === '') {
|
|
|
|
|
throw new BadRequestException("Can't delete a missing profile Image");
|
|
|
|
|
}
|
2023-12-09 23:34:12 -05:00
|
|
|
await this.userRepository.update(auth.user.id, { profileImagePath: '' });
|
2023-11-14 04:10:35 +01:00
|
|
|
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 09:58:25 -05:00
|
|
|
async getProfileImage(id: string): Promise<ImmichFileResponse> {
|
2023-10-31 11:01:32 -04:00
|
|
|
const user = await this.findOrFail(id, {});
|
2023-10-30 17:02:36 -04:00
|
|
|
if (!user.profileImagePath) {
|
|
|
|
|
throw new NotFoundException('User does not have a profile image');
|
|
|
|
|
}
|
2023-12-12 09:58:25 -05:00
|
|
|
|
|
|
|
|
return new ImmichFileResponse({
|
|
|
|
|
path: user.profileImagePath,
|
|
|
|
|
contentType: 'image/jpeg',
|
2023-12-18 11:33:46 -05:00
|
|
|
cacheControl: CacheControl.NONE,
|
2023-12-12 09:58:25 -05:00
|
|
|
});
|
2022-05-27 22:15:35 -05:00
|
|
|
}
|
2023-01-16 13:09:04 -05:00
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
2024-01-12 18:43:36 -06:00
|
|
|
await this.userRepository.syncUsage();
|
2024-03-15 14:16:54 +01:00
|
|
|
return JobStatus.SUCCESS;
|
2024-01-12 18:43:36 -06:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
async handleUserDeleteCheck(): Promise<JobStatus> {
|
2023-02-25 09:12:03 -05:00
|
|
|
const users = await this.userRepository.getDeletedUsers();
|
2024-03-06 00:45:40 -05:00
|
|
|
const config = await this.configCore.getConfig();
|
2024-01-01 15:45:42 -05:00
|
|
|
await this.jobRepository.queueAll(
|
|
|
|
|
users.flatMap((user) =>
|
2024-03-06 00:45:40 -05:00
|
|
|
this.isReadyForDeletion(user, config.user.deleteDelay)
|
|
|
|
|
? [{ name: JobName.USER_DELETION, data: { id: user.id } }]
|
|
|
|
|
: [],
|
2024-01-01 15:45:42 -05:00
|
|
|
),
|
|
|
|
|
);
|
2024-03-15 14:16:54 +01:00
|
|
|
return JobStatus.SUCCESS;
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
2024-03-06 00:45:40 -05:00
|
|
|
const config = await this.configCore.getConfig();
|
2023-10-31 11:01:32 -04:00
|
|
|
const user = await this.userRepository.get(id, { withDeleted: true });
|
2023-05-26 15:43:24 -04:00
|
|
|
if (!user) {
|
2024-03-15 14:16:54 +01:00
|
|
|
return JobStatus.FAILED;
|
2023-05-26 15:43:24 -04:00
|
|
|
}
|
2023-02-25 09:12:03 -05:00
|
|
|
|
|
|
|
|
// just for extra protection here
|
2024-03-08 17:49:39 -05:00
|
|
|
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
2023-05-26 15:43:24 -04:00
|
|
|
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
|
2024-03-15 14:16:54 +01:00
|
|
|
return JobStatus.SKIPPED;
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.logger.log(`Deleting user: ${user.id}`);
|
|
|
|
|
|
2023-05-26 15:43:24 -04:00
|
|
|
const folders = [
|
2023-10-23 17:52:21 +02:00
|
|
|
StorageCore.getLibraryFolder(user),
|
|
|
|
|
StorageCore.getFolderLocation(StorageFolder.UPLOAD, user.id),
|
|
|
|
|
StorageCore.getFolderLocation(StorageFolder.PROFILE, user.id),
|
|
|
|
|
StorageCore.getFolderLocation(StorageFolder.THUMBNAILS, user.id),
|
|
|
|
|
StorageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, user.id),
|
2023-05-26 15:43:24 -04:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const folder of folders) {
|
|
|
|
|
this.logger.warn(`Removing user from filesystem: ${folder}`);
|
|
|
|
|
await this.storageRepository.unlinkDir(folder, { recursive: true, force: true });
|
|
|
|
|
}
|
2023-02-25 09:12:03 -05:00
|
|
|
|
2023-05-26 15:43:24 -04:00
|
|
|
this.logger.warn(`Removing user from database: ${user.id}`);
|
|
|
|
|
await this.albumRepository.deleteAll(user.id);
|
|
|
|
|
await this.userRepository.delete(user, true);
|
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
return JobStatus.SUCCESS;
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|
|
|
|
|
|
2024-03-06 00:45:40 -05:00
|
|
|
private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean {
|
2023-02-25 09:12:03 -05:00
|
|
|
if (!user.deletedAt) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-06 00:45:40 -05:00
|
|
|
return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt);
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|
2023-10-30 19:38:34 -04:00
|
|
|
|
2023-10-31 11:01:32 -04:00
|
|
|
private async findOrFail(id: string, options: UserFindOptions) {
|
|
|
|
|
const user = await this.userRepository.get(id, options);
|
2023-10-30 19:38:34 -04:00
|
|
|
if (!user) {
|
|
|
|
|
throw new BadRequestException('User not found');
|
|
|
|
|
}
|
|
|
|
|
return user;
|
|
|
|
|
}
|
2022-02-03 10:06:44 -06:00
|
|
|
}
|