mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server): user preferences (#9736)
* refactor(server): user endpoints * feat(server): user preferences * mobile: user preference * wording --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
1f9158c545
commit
0fc6d69824
39 changed files with 1392 additions and 327 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
UserAdminCreateDto,
|
||||
UserAdminDeleteDto,
|
||||
|
|
@ -55,6 +56,22 @@ export class UserAdminController {
|
|||
return this.service.delete(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/preferences')
|
||||
@Authenticated()
|
||||
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.getPreferences(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/preferences')
|
||||
@Authenticated()
|
||||
updateUserPreferencesAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: UserPreferencesUpdateDto,
|
||||
): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.updatePreferences(auth, id, dto);
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@Authenticated({ admin: true })
|
||||
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
|
|
@ -52,6 +53,21 @@ export class UserController {
|
|||
return this.service.updateMe(auth, dto);
|
||||
}
|
||||
|
||||
@Get('me/preferences')
|
||||
@Authenticated()
|
||||
getMyPreferences(@Auth() auth: AuthDto): UserPreferencesResponseDto {
|
||||
return this.service.getMyPreferences(auth);
|
||||
}
|
||||
|
||||
@Put('me/preferences')
|
||||
@Authenticated()
|
||||
updateMyPreferences(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: UserPreferencesUpdateDto,
|
||||
): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.updateMyPreferences(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
|
|
|
|||
47
server/src/dtos/user-preferences.dto.ts
Normal file
47
server/src/dtos/user-preferences.dto.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, ValidateNested } from 'class-validator';
|
||||
import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
class AvatarUpdate {
|
||||
@Optional()
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
color?: UserAvatarColor;
|
||||
}
|
||||
|
||||
class MemoryUpdate {
|
||||
@ValidateBoolean({ optional: true })
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export class UserPreferencesUpdateDto {
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
@Type(() => AvatarUpdate)
|
||||
avatar?: AvatarUpdate;
|
||||
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
@Type(() => MemoryUpdate)
|
||||
memories?: MemoryUpdate;
|
||||
}
|
||||
|
||||
class AvatarResponse {
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
color!: UserAvatarColor;
|
||||
}
|
||||
|
||||
class MemoryResponse {
|
||||
enabled!: boolean;
|
||||
}
|
||||
|
||||
export class UserPreferencesResponseDto implements UserPreferences {
|
||||
memories!: MemoryResponse;
|
||||
avatar!: AvatarResponse;
|
||||
}
|
||||
|
||||
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {
|
||||
return preferences;
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
|
||||
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
|
|
@ -22,14 +22,6 @@ export class UserUpdateMeDto {
|
|||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
memoriesEnabled?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor?: UserAvatarColor;
|
||||
}
|
||||
|
||||
export class UserResponseDto {
|
||||
|
|
@ -37,7 +29,6 @@ export class UserResponseDto {
|
|||
name!: string;
|
||||
email!: string;
|
||||
profileImagePath!: string;
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor!: UserAvatarColor;
|
||||
}
|
||||
|
|
@ -75,9 +66,6 @@ export class UserAdminCreateDto {
|
|||
@Transform(toSanitized)
|
||||
storageLabel?: string | null;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
memoriesEnabled?: boolean;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
|
|
@ -116,14 +104,6 @@ export class UserAdminUpdateDto {
|
|||
@ValidateBoolean({ optional: true })
|
||||
shouldChangePassword?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
memoriesEnabled?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor?: UserAvatarColor;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
|
|
@ -144,7 +124,6 @@ export class UserAdminResponseDto extends UserResponseDto {
|
|||
deletedAt!: Date | null;
|
||||
updatedAt!: Date;
|
||||
oauthId!: string;
|
||||
memoriesEnabled?: boolean;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes!: number | null;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
|
|
@ -163,7 +142,6 @@ export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
|||
deletedAt: entity.deletedAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
oauthId: entity.oauthId,
|
||||
memoriesEnabled: getPreferences(entity).memories.enabled,
|
||||
quotaSizeInBytes: entity.quotaSizeInBytes,
|
||||
quotaUsageInBytes: entity.quotaUsageInBytes,
|
||||
status: entity.status,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { BadRequestException, ForbiddenException, Inject, Injectable } from '@ne
|
|||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { UserCore } from 'src/cores/user.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
UserAdminCreateDto,
|
||||
UserAdminDeleteDto,
|
||||
|
|
@ -17,7 +18,7 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
|||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||
import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
|
||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
export class UserAdminService {
|
||||
|
|
@ -40,18 +41,8 @@ export class UserAdminService {
|
|||
}
|
||||
|
||||
async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
|
||||
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 { notify, ...rest } = dto;
|
||||
const user = await this.userCore.createUser(rest);
|
||||
|
||||
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
|
||||
if (notify) {
|
||||
|
|
@ -72,25 +63,6 @@ export class UserAdminService {
|
|||
await this.userRepository.syncUsage(id);
|
||||
}
|
||||
|
||||
// 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(id, {
|
||||
key: UserMetadataKey.PREFERENCES,
|
||||
value: getPreferencesPartial(user, newPreferences),
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.email) {
|
||||
const duplicate = await this.userRepository.getByEmail(dto.email);
|
||||
if (duplicate && duplicate.id !== id) {
|
||||
|
|
@ -144,6 +116,24 @@ export class UserAdminService {
|
|||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
|
||||
const user = await this.findOrFail(id, { withDeleted: false });
|
||||
const preferences = getPreferences(user);
|
||||
return mapPreferences(preferences);
|
||||
}
|
||||
|
||||
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
|
||||
const user = await this.findOrFail(id, { withDeleted: false });
|
||||
const preferences = mergePreferences(user, dto);
|
||||
|
||||
await this.userRepository.upsertMetadata(user.id, {
|
||||
key: UserMetadataKey.PREFERENCES,
|
||||
value: getPreferencesPartial(user, preferences),
|
||||
});
|
||||
|
||||
return mapPreferences(preferences);
|
||||
}
|
||||
|
||||
private async findOrFail(id: string, options: UserFindOptions) {
|
||||
const user = await this.userRepository.get(id, options);
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { SALT_ROUNDS } from 'src/constants';
|
|||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
|
|
@ -16,7 +17,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
|
||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
|
@ -45,25 +46,6 @@ export class UserService {
|
|||
}
|
||||
|
||||
async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
|
||||
// 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(user.id, {
|
||||
key: UserMetadataKey.PREFERENCES,
|
||||
value: getPreferencesPartial(user, newPreferences),
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.email) {
|
||||
const duplicate = await this.userRepository.getByEmail(dto.email);
|
||||
if (duplicate && duplicate.id !== user.id) {
|
||||
|
|
@ -87,6 +69,22 @@ export class UserService {
|
|||
return mapUserAdmin(updatedUser);
|
||||
}
|
||||
|
||||
getMyPreferences({ user }: AuthDto): UserPreferencesResponseDto {
|
||||
const preferences = getPreferences(user);
|
||||
return mapPreferences(preferences);
|
||||
}
|
||||
|
||||
async updateMyPreferences({ user }: AuthDto, dto: UserPreferencesUpdateDto) {
|
||||
const preferences = mergePreferences(user, dto);
|
||||
|
||||
await this.userRepository.upsertMetadata(user.id, {
|
||||
key: UserMetadataKey.PREFERENCES,
|
||||
value: getPreferencesPartial(user, preferences),
|
||||
});
|
||||
|
||||
return mapPreferences(preferences);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<UserResponseDto> {
|
||||
const user = await this.findOrFail(id, { withDeleted: false });
|
||||
return mapUser(user);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { UserMetadataKey, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { getKeysDeep } from 'src/utils/misc';
|
||||
|
|
@ -37,3 +38,12 @@ export const getPreferencesPartial = (user: { email: string }, newPreferences: U
|
|||
|
||||
return partial;
|
||||
};
|
||||
|
||||
export const mergePreferences = (user: UserEntity, dto: UserPreferencesUpdateDto) => {
|
||||
const preferences = getPreferences(user);
|
||||
for (const key of getKeysDeep(dto)) {
|
||||
_.set(preferences, key, _.get(dto, key));
|
||||
}
|
||||
|
||||
return preferences;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue