diff --git a/server/src/database.ts b/server/src/database.ts index 33a877102f..45e7cad490 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,6 +1,5 @@ -import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; -import { OnThisDayData } from 'src/types'; +import { OnThisDayData, UserMetadataItem } from 'src/types'; export type AuthUser = { id: string; @@ -96,7 +95,7 @@ export type UserAdmin = User & { quotaSizeInBytes: number | null; quotaUsageInBytes: number; status: UserStatus; - metadata: UserMetadataEntity[]; + metadata: UserMetadataItem[]; }; export type Asset = { diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 98216147b7..a97116cf35 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { Activity } from 'src/database'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { UserEntity } from 'src/entities/user.entity'; import { Optional, ValidateUUID } from 'src/validation'; export enum ReactionType { @@ -75,6 +74,6 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => { createdAt: activity.createdAt, comment: activity.comment, type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT, - user: mapUser(activity.user as unknown as UserEntity), + user: mapUser(activity.user), }; }; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 5a393a2d71..fe92838fdb 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; -import { UserPreferences } from 'src/entities/user-metadata.entity'; import { UserAvatarColor } from 'src/enum'; +import { UserPreferences } from 'src/types'; import { Optional, ValidateBoolean } from 'src/validation'; class AvatarUpdate { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index afcd13f0e9..851d4d3921 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; -import { UserMetadataEntity, UserMetadataItem } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { UserMetadataItem } from 'src/types'; import { getPreferences } from 'src/utils/preferences'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; @@ -143,8 +143,9 @@ export class UserAdminResponseDto extends UserResponseDto { } export function mapUserAdmin(entity: UserEntity | UserAdmin): UserAdminResponseDto { - const license = (entity.metadata as UserMetadataItem[])?.find( - (item): item is UserMetadataEntity => item.key === UserMetadataKey.LICENSE, + const metadata = entity.metadata || []; + const license = metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.LICENSE, )?.value; return { ...mapUser(entity), diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts deleted file mode 100644 index 065f4deac3..0000000000 --- a/server/src/entities/user-metadata.entity.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { UserEntity } from 'src/entities/user.entity'; -import { UserAvatarColor, UserMetadataKey } from 'src/enum'; -import { DeepPartial } from 'src/types'; -import { HumanReadableSize } from 'src/utils/bytes'; - -export type UserMetadataItem = { - key: T; - value: UserMetadata[T]; -}; - -export class UserMetadataEntity implements UserMetadataItem { - userId!: string; - user?: UserEntity; - key!: T; - value!: UserMetadata[T]; -} - -export interface UserPreferences { - folders: { - enabled: boolean; - sidebarWeb: boolean; - }; - memories: { - enabled: boolean; - }; - people: { - enabled: boolean; - sidebarWeb: boolean; - }; - ratings: { - enabled: boolean; - }; - sharedLinks: { - enabled: boolean; - sidebarWeb: boolean; - }; - tags: { - enabled: boolean; - sidebarWeb: boolean; - }; - avatar: { - color: UserAvatarColor; - }; - emailNotifications: { - enabled: boolean; - albumInvite: boolean; - albumUpdate: boolean; - }; - download: { - archiveSize: number; - includeEmbeddedVideos: boolean; - }; - purchase: { - showSupportBadge: boolean; - hideBuyButtonUntil: string; - }; -} - -export const getDefaultPreferences = (user: { email: string }): UserPreferences => { - const values = Object.values(UserAvatarColor); - const randomIndex = Math.floor( - [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, - ); - - return { - folders: { - enabled: false, - sidebarWeb: false, - }, - memories: { - enabled: true, - }, - people: { - enabled: true, - sidebarWeb: false, - }, - sharedLinks: { - enabled: true, - sidebarWeb: false, - }, - ratings: { - enabled: false, - }, - tags: { - enabled: false, - sidebarWeb: false, - }, - avatar: { - color: values[randomIndex], - }, - emailNotifications: { - enabled: true, - albumInvite: true, - albumUpdate: true, - }, - download: { - archiveSize: HumanReadableSize.GiB * 4, - includeEmbeddedVideos: false, - }, - purchase: { - showSupportBadge: true, - hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), - }, - }; -}; - -export interface UserMetadata extends Record> { - [UserMetadataKey.PREFERENCES]: DeepPartial; - [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; -} diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 5035f96274..96c574c83d 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -2,8 +2,8 @@ import { ExpressionBuilder } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { DB } from 'src/db'; import { AssetEntity } from 'src/entities/asset.entity'; -import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserStatus } from 'src/enum'; +import { UserMetadataItem } from 'src/types'; export class UserEntity { id!: string; @@ -23,7 +23,7 @@ export class UserEntity { assets!: AssetEntity[]; quotaSizeInBytes!: number | null; quotaUsageInBytes!: number; - metadata!: UserMetadataEntity[]; + metadata!: UserMetadataItem[]; profileChangedAt!: Date; } diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index c254085fd2..5912f60687 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -5,10 +5,10 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns, UserAdmin } from 'src/database'; import { DB, UserMetadata as DbUserMetadata } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; import { UserEntity, withMetadata } from 'src/entities/user.entity'; import { AssetType, UserStatus } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; +import { UserMetadata, UserMetadataItem } from 'src/types'; import { asUuid } from 'src/utils/database'; type Upsert = Insertable; diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index e71b3bf9f9..6d03acaf80 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -1,7 +1,7 @@ -import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; import { UserMetadataKey } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { UserMetadata, UserMetadataItem } from 'src/types'; @Table('user_metadata') export class UserMetadataTable implements UserMetadataItem { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 0c5ad3099d..3c8bfa7d95 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; -import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { AuthType, Permission } from 'src/enum'; import { AuthService } from 'src/services/auth.service'; +import { UserMetadataItem } from 'src/types'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -230,7 +230,7 @@ describe('AuthService', () => { ...dto, id: 'admin', createdAt: new Date('2021-01-01'), - metadata: [] as UserMetadataEntity[], + metadata: [] as UserMetadataItem[], } as UserEntity); await expect(sut.adminSignUp(dto)).resolves.toMatchObject({ diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 89f211b297..823f1614ea 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -357,8 +357,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: false, albumInvite: true } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -374,8 +372,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumInvite: false } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -391,8 +387,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumInvite: true } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -414,8 +408,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumInvite: true } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -443,8 +435,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumInvite: true } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -476,8 +466,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumInvite: true } }, - userId: userStub.user1.id, - user: userStub.user1, }, ], }); @@ -536,8 +524,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: false, albumUpdate: true } }, - user: userStub.user1, - userId: userStub.user1.id, }, ], }); @@ -559,8 +545,6 @@ describe(NotificationService.name, () => { { key: UserMetadataKey.PREFERENCES, value: { emailNotifications: { enabled: true, albumUpdate: false } }, - user: userStub.user1, - userId: userStub.user1.id, }, ], }); diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index ef06b6f4b1..d1859ed419 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -8,12 +8,11 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; -import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum'; import { UserFindOptions } from 'src/repositories/user.repository'; import { BaseService } from 'src/services/base.service'; -import { JobOf } from 'src/types'; +import { JobOf, UserMetadataItem } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @@ -135,7 +134,7 @@ export class UserService extends BaseService { const metadata = await this.userRepository.getMetadata(auth.user.id); const license = metadata.find( - (item): item is UserMetadataEntity => item.key === UserMetadataKey.LICENSE, + (item): item is UserMetadataItem => item.key === UserMetadataKey.LICENSE, ); if (!license) { throw new NotFoundException(); diff --git a/server/src/types.ts b/server/src/types.ts index 68207c4b9e..88ba644739 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -11,6 +11,8 @@ import { SyncEntityType, SystemMetadataKey, TranscodeTarget, + UserAvatarColor, + UserMetadataKey, VideoCodec, } from 'src/enum'; @@ -455,3 +457,54 @@ export interface SystemMetadata extends Record = { + key: T; + value: UserMetadata[T]; +}; + +export interface UserPreferences { + folders: { + enabled: boolean; + sidebarWeb: boolean; + }; + memories: { + enabled: boolean; + }; + people: { + enabled: boolean; + sidebarWeb: boolean; + }; + ratings: { + enabled: boolean; + }; + sharedLinks: { + enabled: boolean; + sidebarWeb: boolean; + }; + tags: { + enabled: boolean; + sidebarWeb: boolean; + }; + avatar: { + color: UserAvatarColor; + }; + emailNotifications: { + enabled: boolean; + albumInvite: boolean; + albumUpdate: boolean; + }; + download: { + archiveSize: number; + includeEmbeddedVideos: boolean; + }; + purchase: { + showSupportBadge: boolean; + hideBuyButtonUntil: string; + }; +} + +export interface UserMetadata extends Record> { + [UserMetadataKey.PREFERENCES]: DeepPartial; + [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; +} diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index 14e61f1919..584c5300cd 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -1,10 +1,58 @@ import _ from 'lodash'; import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; -import { UserMetadataItem, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; -import { UserMetadataKey } from 'src/enum'; -import { DeepPartial } from 'src/types'; +import { UserAvatarColor, UserMetadataKey } from 'src/enum'; +import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types'; +import { HumanReadableSize } from 'src/utils/bytes'; import { getKeysDeep } from 'src/utils/misc'; +const getDefaultPreferences = (user: { email: string }): UserPreferences => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, + ); + + return { + folders: { + enabled: false, + sidebarWeb: false, + }, + memories: { + enabled: true, + }, + people: { + enabled: true, + sidebarWeb: false, + }, + sharedLinks: { + enabled: true, + sidebarWeb: false, + }, + ratings: { + enabled: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, + avatar: { + color: values[randomIndex], + }, + emailNotifications: { + enabled: true, + albumInvite: true, + albumUpdate: true, + }, + download: { + archiveSize: HumanReadableSize.GiB * 4, + includeEmbeddedVideos: false, + }, + purchase: { + showSupportBadge: true, + hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), + }, + }; +}; + export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => { const preferences = getDefaultPreferences({ email }); const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES); diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 0ed1502fb9..844b8c61b9 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -38,7 +38,6 @@ export const userStub = { assets: [], metadata: [ { - userId: authStub.user1.user.id, key: UserMetadataKey.PREFERENCES, value: { avatar: { color: UserAvatarColor.PRIMARY } }, },