mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 12:15:47 -04:00
refactor: remove user entity (#17498)
This commit is contained in:
parent
9e49783e49
commit
94dba29298
6
server/src/db.d.ts
vendored
6
server/src/db.d.ts
vendored
@ -17,7 +17,7 @@ import {
|
|||||||
SyncEntityType,
|
SyncEntityType,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { OnThisDayData } from 'src/types';
|
import {OnThisDayData, UserMetadataItem} from 'src/types';
|
||||||
|
|
||||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
|
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
|
||||||
|
|
||||||
@ -412,10 +412,8 @@ export interface TypeormMetadata {
|
|||||||
value: string | null;
|
value: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserMetadata {
|
export interface UserMetadata extends UserMetadataItem {
|
||||||
key: string;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
value: Json;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersAudit {
|
export interface UsersAudit {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database';
|
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { ImmichCookie } from 'src/enum';
|
import { ImmichCookie } from 'src/enum';
|
||||||
import { toEmail } from 'src/validation';
|
import { toEmail } from 'src/validation';
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ export class LoginResponseDto {
|
|||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto {
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
userId: entity.id,
|
userId: entity.id,
|
||||||
|
@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
@ -42,13 +41,13 @@ export class UserLicense {
|
|||||||
activatedAt!: Date;
|
activatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapUser = (entity: UserEntity | User): UserResponseDto => {
|
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
email: entity.email,
|
email: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
avatarColor: getPreferences(entity.email, (entity as UserEntity).metadata || []).avatar.color,
|
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color,
|
||||||
profileChangedAt: entity.profileChangedAt,
|
profileChangedAt: entity.profileChangedAt,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -142,7 +141,7 @@ export class UserAdminResponseDto extends UserResponseDto {
|
|||||||
license!: UserLicense | null;
|
license!: UserLicense | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapUserAdmin(entity: UserEntity | UserAdmin): UserAdminResponseDto {
|
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
|
||||||
const metadata = entity.metadata || [];
|
const metadata = entity.metadata || [];
|
||||||
const license = metadata.find(
|
const license = metadata.find(
|
||||||
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
|
import { User } from 'src/database';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole } from 'src/enum';
|
||||||
|
|
||||||
export class AlbumUserEntity {
|
export class AlbumUserEntity {
|
||||||
albumId!: string;
|
albumId!: string;
|
||||||
userId!: string;
|
userId!: string;
|
||||||
album!: AlbumEntity;
|
album!: AlbumEntity;
|
||||||
user!: UserEntity;
|
user!: User;
|
||||||
role!: AlbumUserRole;
|
role!: AlbumUserRole;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import { User } from 'src/database';
|
||||||
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
|
|
||||||
export class AlbumEntity {
|
export class AlbumEntity {
|
||||||
id!: string;
|
id!: string;
|
||||||
owner!: UserEntity;
|
owner!: User;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
albumName!: string;
|
albumName!: string;
|
||||||
description!: string;
|
description!: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { Tag } from 'src/database';
|
import { Tag, User } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
@ -9,7 +9,6 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
|||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { StackEntity } from 'src/entities/stack.entity';
|
import { StackEntity } from 'src/entities/stack.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||||
@ -20,7 +19,7 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
|||||||
export class AssetEntity {
|
export class AssetEntity {
|
||||||
id!: string;
|
id!: string;
|
||||||
deviceAssetId!: string;
|
deviceAssetId!: string;
|
||||||
owner!: UserEntity;
|
owner!: User;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
libraryId?: string | null;
|
libraryId?: string | null;
|
||||||
deviceId!: string;
|
deviceId!: string;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
|
|
||||||
export class PersonEntity {
|
export class PersonEntity {
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -7,7 +6,6 @@ export class PersonEntity {
|
|||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
owner!: UserEntity;
|
|
||||||
name!: string;
|
name!: string;
|
||||||
birthDate!: Date | string | null;
|
birthDate!: Date | string | null;
|
||||||
thumbnailPath!: string;
|
thumbnailPath!: string;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
|
|
||||||
export class SharedLinkEntity {
|
export class SharedLinkEntity {
|
||||||
@ -8,7 +7,6 @@ export class SharedLinkEntity {
|
|||||||
description!: string | null;
|
description!: string | null;
|
||||||
password!: string | null;
|
password!: string | null;
|
||||||
userId!: string;
|
userId!: string;
|
||||||
user!: UserEntity;
|
|
||||||
key!: Buffer; // use to access the inidividual asset
|
key!: Buffer; // use to access the inidividual asset
|
||||||
type!: SharedLinkType;
|
type!: SharedLinkType;
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
|
|
||||||
export class StackEntity {
|
export class StackEntity {
|
||||||
id!: string;
|
id!: string;
|
||||||
owner!: UserEntity;
|
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
assets!: AssetEntity[];
|
assets!: AssetEntity[];
|
||||||
primaryAsset!: AssetEntity;
|
primaryAsset!: AssetEntity;
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import { ExpressionBuilder } from 'kysely';
|
|
||||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
||||||
import { DB } from 'src/db';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { UserStatus } from 'src/enum';
|
|
||||||
import { UserMetadataItem } from 'src/types';
|
|
||||||
|
|
||||||
export class UserEntity {
|
|
||||||
id!: string;
|
|
||||||
name!: string;
|
|
||||||
isAdmin!: boolean;
|
|
||||||
email!: string;
|
|
||||||
storageLabel!: string | null;
|
|
||||||
password?: string;
|
|
||||||
oauthId!: string;
|
|
||||||
profileImagePath!: string;
|
|
||||||
shouldChangePassword!: boolean;
|
|
||||||
createdAt!: Date;
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
status!: UserStatus;
|
|
||||||
updatedAt!: Date;
|
|
||||||
updateId?: string;
|
|
||||||
assets!: AssetEntity[];
|
|
||||||
quotaSizeInBytes!: number | null;
|
|
||||||
quotaUsageInBytes!: number;
|
|
||||||
metadata!: UserMetadataItem[];
|
|
||||||
profileChangedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const withMetadata = (eb: ExpressionBuilder<DB, 'users'>) => {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb.selectFrom('user_metadata').selectAll('user_metadata').whereRef('users.id', '=', 'user_metadata.userId'),
|
|
||||||
).as('metadata');
|
|
||||||
};
|
|
@ -24,7 +24,8 @@ select
|
|||||||
from
|
from
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
"user_metadata".*
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
from
|
from
|
||||||
"user_metadata"
|
"user_metadata"
|
||||||
where
|
where
|
||||||
@ -54,7 +55,21 @@ select
|
|||||||
"shouldChangePassword",
|
"shouldChangePassword",
|
||||||
"storageLabel",
|
"storageLabel",
|
||||||
"quotaSizeInBytes",
|
"quotaSizeInBytes",
|
||||||
"quotaUsageInBytes"
|
"quotaUsageInBytes",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
|
from
|
||||||
|
"user_metadata"
|
||||||
|
where
|
||||||
|
"users"."id" = "user_metadata"."userId"
|
||||||
|
) as agg
|
||||||
|
) as "metadata"
|
||||||
from
|
from
|
||||||
"users"
|
"users"
|
||||||
where
|
where
|
||||||
@ -87,7 +102,21 @@ select
|
|||||||
"shouldChangePassword",
|
"shouldChangePassword",
|
||||||
"storageLabel",
|
"storageLabel",
|
||||||
"quotaSizeInBytes",
|
"quotaSizeInBytes",
|
||||||
"quotaUsageInBytes"
|
"quotaUsageInBytes",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
|
from
|
||||||
|
"user_metadata"
|
||||||
|
where
|
||||||
|
"users"."id" = "user_metadata"."userId"
|
||||||
|
) as agg
|
||||||
|
) as "metadata"
|
||||||
from
|
from
|
||||||
"users"
|
"users"
|
||||||
where
|
where
|
||||||
@ -135,7 +164,21 @@ select
|
|||||||
"shouldChangePassword",
|
"shouldChangePassword",
|
||||||
"storageLabel",
|
"storageLabel",
|
||||||
"quotaSizeInBytes",
|
"quotaSizeInBytes",
|
||||||
"quotaUsageInBytes"
|
"quotaUsageInBytes",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
|
from
|
||||||
|
"user_metadata"
|
||||||
|
where
|
||||||
|
"users"."id" = "user_metadata"."userId"
|
||||||
|
) as agg
|
||||||
|
) as "metadata"
|
||||||
from
|
from
|
||||||
"users"
|
"users"
|
||||||
where
|
where
|
||||||
@ -174,7 +217,8 @@ select
|
|||||||
from
|
from
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
"user_metadata".*
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
from
|
from
|
||||||
"user_metadata"
|
"user_metadata"
|
||||||
where
|
where
|
||||||
@ -210,7 +254,8 @@ select
|
|||||||
from
|
from
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
"user_metadata".*
|
"user_metadata"."key",
|
||||||
|
"user_metadata"."value"
|
||||||
from
|
from
|
||||||
"user_metadata"
|
"user_metadata"
|
||||||
where
|
where
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||||
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns, UserAdmin } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
|
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { UserEntity, withMetadata } from 'src/entities/user.entity';
|
|
||||||
import { AssetType, UserStatus } from 'src/enum';
|
import { AssetType, UserStatus } from 'src/enum';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { UserMetadata, UserMetadataItem } from 'src/types';
|
import { UserMetadata, UserMetadataItem } from 'src/types';
|
||||||
@ -32,12 +32,21 @@ export interface UserFindOptions {
|
|||||||
withDeleted?: boolean;
|
withDeleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const withMetadata = (eb: ExpressionBuilder<DB, 'users'>) => {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('user_metadata')
|
||||||
|
.select(['user_metadata.key', 'user_metadata.value'])
|
||||||
|
.whereRef('users.id', '=', 'user_metadata.userId'),
|
||||||
|
).as('metadata');
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository {
|
export class UserRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
||||||
get(userId: string, options: UserFindOptions): Promise<UserEntity | undefined> {
|
get(userId: string, options: UserFindOptions) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
return this.db
|
return this.db
|
||||||
@ -46,7 +55,7 @@ export class UserRepository {
|
|||||||
.select(withMetadata)
|
.select(withMetadata)
|
||||||
.where('users.id', '=', userId)
|
.where('users.id', '=', userId)
|
||||||
.$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
.$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
||||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata(userId: string) {
|
getMetadata(userId: string) {
|
||||||
@ -58,13 +67,14 @@ export class UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
@GenerateSql()
|
||||||
getAdmin(): Promise<UserEntity | undefined> {
|
getAdmin() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(columns.userAdmin)
|
.select(columns.userAdmin)
|
||||||
|
.select(withMetadata)
|
||||||
.where('users.isAdmin', '=', true)
|
.where('users.isAdmin', '=', true)
|
||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
@GenerateSql()
|
||||||
@ -80,34 +90,36 @@ export class UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.EMAIL] })
|
@GenerateSql({ params: [DummyValue.EMAIL] })
|
||||||
getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | undefined> {
|
getByEmail(email: string, withPassword?: boolean) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(columns.userAdmin)
|
.select(columns.userAdmin)
|
||||||
|
.select(withMetadata)
|
||||||
.$if(!!withPassword, (eb) => eb.select('password'))
|
.$if(!!withPassword, (eb) => eb.select('password'))
|
||||||
.where('email', '=', email)
|
.where('email', '=', email)
|
||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
getByStorageLabel(storageLabel: string): Promise<UserEntity | undefined> {
|
getByStorageLabel(storageLabel: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(columns.userAdmin)
|
.select(columns.userAdmin)
|
||||||
.where('users.storageLabel', '=', storageLabel)
|
.where('users.storageLabel', '=', storageLabel)
|
||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
getByOAuthId(oauthId: string): Promise<UserEntity | undefined> {
|
getByOAuthId(oauthId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(columns.userAdmin)
|
.select(columns.userAdmin)
|
||||||
|
.select(withMetadata)
|
||||||
.where('users.oauthId', '=', oauthId)
|
.where('users.oauthId', '=', oauthId)
|
||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DateTime.now().minus({ years: 1 })] })
|
@GenerateSql({ params: [DateTime.now().minus({ years: 1 })] })
|
||||||
@ -126,18 +138,19 @@ export class UserRepository {
|
|||||||
.select(withMetadata)
|
.select(withMetadata)
|
||||||
.$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
.$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
||||||
.orderBy('createdAt', 'desc')
|
.orderBy('createdAt', 'desc')
|
||||||
.execute() as Promise<UserAdmin[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: Insertable<UserTable>): Promise<UserEntity> {
|
async create(dto: Insertable<UserTable>) {
|
||||||
return this.db
|
return this.db
|
||||||
.insertInto('users')
|
.insertInto('users')
|
||||||
.values(dto)
|
.values(dto)
|
||||||
.returning(columns.userAdmin)
|
.returning(columns.userAdmin)
|
||||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
.returning(withMetadata)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, dto: Updateable<UserTable>): Promise<UserEntity> {
|
update(id: string, dto: Updateable<UserTable>) {
|
||||||
return this.db
|
return this.db
|
||||||
.updateTable('users')
|
.updateTable('users')
|
||||||
.set(dto)
|
.set(dto)
|
||||||
@ -145,17 +158,17 @@ export class UserRepository {
|
|||||||
.where('users.deletedAt', 'is', null)
|
.where('users.deletedAt', 'is', null)
|
||||||
.returning(columns.userAdmin)
|
.returning(columns.userAdmin)
|
||||||
.returning(withMetadata)
|
.returning(withMetadata)
|
||||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
restore(id: string): Promise<UserEntity> {
|
restore(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.updateTable('users')
|
.updateTable('users')
|
||||||
.set({ status: UserStatus.ACTIVE, deletedAt: null })
|
.set({ status: UserStatus.ACTIVE, deletedAt: null })
|
||||||
.where('users.id', '=', asUuid(id))
|
.where('users.id', '=', asUuid(id))
|
||||||
.returning(columns.userAdmin)
|
.returning(columns.userAdmin)
|
||||||
.returning(withMetadata)
|
.returning(withMetadata)
|
||||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
|
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
|
||||||
@ -175,14 +188,10 @@ export class UserRepository {
|
|||||||
await this.db.deleteFrom('user_metadata').where('userId', '=', id).where('key', '=', key).execute();
|
await this.db.deleteFrom('user_metadata').where('userId', '=', id).where('key', '=', key).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(user: { id: string }, hard?: boolean): Promise<UserEntity> {
|
delete(user: { id: string }, hard?: boolean) {
|
||||||
return hard
|
return hard
|
||||||
? (this.db.deleteFrom('users').where('id', '=', user.id).execute() as unknown as Promise<UserEntity>)
|
? this.db.deleteFrom('users').where('id', '=', user.id).execute()
|
||||||
: (this.db
|
: this.db.updateTable('users').set({ deletedAt: new Date() }).where('id', '=', user.id).execute();
|
||||||
.updateTable('users')
|
|
||||||
.set({ deletedAt: new Date() })
|
|
||||||
.where('id', '=', user.id)
|
|
||||||
.execute() as unknown as Promise<UserEntity>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
@GenerateSql()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { UserAdmin } from 'src/database';
|
||||||
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AuthType, Permission } from 'src/enum';
|
import { AuthType, Permission } from 'src/enum';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
@ -89,7 +89,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should check the user has a password', async () => {
|
it('should check the user has a password', async () => {
|
||||||
mocks.user.getByEmail.mockResolvedValue({} as UserEntity);
|
mocks.user.getByEmail.mockResolvedValue({} as UserAdmin);
|
||||||
|
|
||||||
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should successfully log the user in', async () => {
|
it('should successfully log the user in', async () => {
|
||||||
const user = { ...factory.user(), password: 'immich_password' } as UserEntity;
|
const user = { ...(factory.user() as UserAdmin), password: 'immich_password' };
|
||||||
const session = factory.session();
|
const session = factory.session();
|
||||||
mocks.user.getByEmail.mockResolvedValue(user);
|
mocks.user.getByEmail.mockResolvedValue(user);
|
||||||
mocks.session.create.mockResolvedValue(session);
|
mocks.session.create.mockResolvedValue(session);
|
||||||
@ -124,7 +124,7 @@ describe('AuthService', () => {
|
|||||||
mocks.user.getByEmail.mockResolvedValue({
|
mocks.user.getByEmail.mockResolvedValue({
|
||||||
email: 'test@immich.com',
|
email: 'test@immich.com',
|
||||||
password: 'hash-password',
|
password: 'hash-password',
|
||||||
} as UserEntity);
|
} as UserAdmin & { password: string });
|
||||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||||
|
|
||||||
await sut.changePassword(auth, dto);
|
await sut.changePassword(auth, dto);
|
||||||
@ -143,7 +143,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when password does not match existing password', async () => {
|
it('should throw when password does not match existing password', async () => {
|
||||||
const auth = { user: { email: 'test@imimch.com' } as UserEntity };
|
const auth = { user: { email: 'test@imimch.com' } as UserAdmin };
|
||||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||||
|
|
||||||
mocks.crypto.compareBcrypt.mockReturnValue(false);
|
mocks.crypto.compareBcrypt.mockReturnValue(false);
|
||||||
@ -151,7 +151,7 @@ describe('AuthService', () => {
|
|||||||
mocks.user.getByEmail.mockResolvedValue({
|
mocks.user.getByEmail.mockResolvedValue({
|
||||||
email: 'test@immich.com',
|
email: 'test@immich.com',
|
||||||
password: 'hash-password',
|
password: 'hash-password',
|
||||||
} as UserEntity);
|
} as UserAdmin & { password: string });
|
||||||
|
|
||||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
});
|
});
|
||||||
@ -163,7 +163,7 @@ describe('AuthService', () => {
|
|||||||
mocks.user.getByEmail.mockResolvedValue({
|
mocks.user.getByEmail.mockResolvedValue({
|
||||||
email: 'test@immich.com',
|
email: 'test@immich.com',
|
||||||
password: '',
|
password: '',
|
||||||
} as UserEntity);
|
} as UserAdmin & { password: string });
|
||||||
|
|
||||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
});
|
});
|
||||||
@ -217,7 +217,7 @@ describe('AuthService', () => {
|
|||||||
const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' };
|
const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' };
|
||||||
|
|
||||||
it('should only allow one admin', async () => {
|
it('should only allow one admin', async () => {
|
||||||
mocks.user.getAdmin.mockResolvedValue({} as UserEntity);
|
mocks.user.getAdmin.mockResolvedValue({} as UserAdmin);
|
||||||
|
|
||||||
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ describe('AuthService', () => {
|
|||||||
id: 'admin',
|
id: 'admin',
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
metadata: [] as UserMetadataItem[],
|
metadata: [] as UserMetadataItem[],
|
||||||
} as UserEntity);
|
} as unknown as UserAdmin);
|
||||||
|
|
||||||
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({
|
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({
|
||||||
avatarColor: expect.any(String),
|
avatarColor: expect.any(String),
|
||||||
@ -294,7 +294,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not accept an expired key', async () => {
|
it('should not accept an expired key', async () => {
|
||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.authenticate({
|
sut.authenticate({
|
||||||
@ -306,7 +306,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not accept a key on a non-shared route', async () => {
|
it('should not accept a key on a non-shared route', async () => {
|
||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.authenticate({
|
sut.authenticate({
|
||||||
@ -318,7 +318,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not accept a key without a user', async () => {
|
it('should not accept a key without a user', async () => {
|
||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired as any);
|
||||||
mocks.user.get.mockResolvedValue(void 0);
|
mocks.user.get.mockResolvedValue(void 0);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -331,7 +331,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should accept a base64url key', async () => {
|
it('should accept a base64url key', async () => {
|
||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid as any);
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -348,7 +348,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should accept a hex key', async () => {
|
it('should accept a hex key', async () => {
|
||||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid as any);
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -717,7 +717,7 @@ describe('AuthService', () => {
|
|||||||
const auth = { user: authUser, apiKey: authApiKey };
|
const auth = { user: authUser, apiKey: authApiKey };
|
||||||
|
|
||||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||||
mocks.user.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
|
mocks.user.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserAdmin);
|
||||||
|
|
||||||
await expect(sut.link(auth, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
|
await expect(sut.link(auth, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
@ -4,6 +4,7 @@ import { parse } from 'cookie';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
|
import { UserAdmin } from 'src/database';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
@ -17,7 +18,6 @@ import {
|
|||||||
mapLoginResponse,
|
mapLoginResponse,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
|
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
|
||||||
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@ -190,7 +190,7 @@ export class AuthService extends BaseService {
|
|||||||
const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.resolveRedirectUri(oauth, dto.url));
|
const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.resolveRedirectUri(oauth, dto.url));
|
||||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
|
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
let user = await this.userRepository.getByOAuthId(profile.sub);
|
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
||||||
|
|
||||||
// link by email
|
// link by email
|
||||||
if (!user && profile.email) {
|
if (!user && profile.email) {
|
||||||
@ -318,7 +318,7 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Invalid API key');
|
throw new UnauthorizedException('Invalid API key');
|
||||||
}
|
}
|
||||||
|
|
||||||
private validatePassword(inputPassword: string, user: UserEntity): boolean {
|
private validatePassword(inputPassword: string, user: { password?: string }): boolean {
|
||||||
if (!user || !user.password) {
|
if (!user || !user.password) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -347,7 +347,7 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Invalid user token');
|
throw new UnauthorizedException('Invalid user token');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) {
|
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||||
const key = this.cryptoRepository.newPassword(32);
|
const key = this.cryptoRepository.newPassword(32);
|
||||||
const token = this.cryptoRepository.hashSha256(key);
|
const token = this.cryptoRepository.hashSha256(key);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import sanitize from 'sanitize-filename';
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserAdmin } from 'src/database';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||||
@ -138,7 +138,7 @@ export class BaseService {
|
|||||||
return checkAccess(this.accessRepository, request);
|
return checkAccess(this.accessRepository, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserEntity> {
|
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> {
|
||||||
const user = await this.userRepository.getByEmail(dto.email);
|
const user = await this.userRepository.getByEmail(dto.email);
|
||||||
if (user) {
|
if (user) {
|
||||||
throw new BadRequestException('User exists');
|
throw new BadRequestException('User exists');
|
||||||
|
@ -13,6 +13,7 @@ import { authStub } from 'test/fixtures/auth.stub';
|
|||||||
import { faceStub } from 'test/fixtures/face.stub';
|
import { faceStub } from 'test/fixtures/face.stub';
|
||||||
import { personStub } from 'test/fixtures/person.stub';
|
import { personStub } from 'test/fixtures/person.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
const responseDto: PersonResponseDto = {
|
const responseDto: PersonResponseDto = {
|
||||||
@ -1279,7 +1280,8 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
describe('mapFace', () => {
|
describe('mapFace', () => {
|
||||||
it('should map a face', () => {
|
it('should map a face', () => {
|
||||||
expect(mapFaces(faceStub.face1, { user: personStub.withName.owner })).toEqual({
|
const authDto = factory.auth({ id: faceStub.face1.person.ownerId });
|
||||||
|
expect(mapFaces(faceStub.face1, authDto)).toEqual({
|
||||||
boundingBoxX1: 0,
|
boundingBoxX1: 0,
|
||||||
boundingBoxX2: 1,
|
boundingBoxX2: 1,
|
||||||
boundingBoxY1: 0,
|
boundingBoxY1: 0,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserAdmin } from 'src/database';
|
||||||
import { CacheControl, JobName, UserMetadataKey } from 'src/enum';
|
import { CacheControl, JobName, UserMetadataKey } from 'src/enum';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
@ -214,7 +214,7 @@ describe(UserService.name, () => {
|
|||||||
|
|
||||||
describe('handleUserDelete', () => {
|
describe('handleUserDelete', () => {
|
||||||
it('should skip users not ready for deletion', async () => {
|
it('should skip users not ready for deletion', async () => {
|
||||||
const user = { id: 'user-1', deletedAt: makeDeletedAt(5) } as UserEntity;
|
const user = { id: 'user-1', deletedAt: makeDeletedAt(5) } as UserAdmin;
|
||||||
|
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
|
||||||
@ -225,7 +225,7 @@ describe(UserService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete the user and associated assets', async () => {
|
it('should delete the user and associated assets', async () => {
|
||||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10) } as UserEntity;
|
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10) } as UserAdmin;
|
||||||
const options = { force: true, recursive: true };
|
const options = { force: true, recursive: true };
|
||||||
|
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
@ -242,7 +242,7 @@ describe(UserService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete the library path for a storage label', async () => {
|
it('should delete the library path for a storage label', async () => {
|
||||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10), storageLabel: 'admin' } as UserEntity;
|
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10), storageLabel: 'admin' } as UserAdmin;
|
||||||
|
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Updateable } from 'kysely';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
@ -8,9 +9,9 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
|||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { JobOf, UserMetadataItem } from 'src/types';
|
import { JobOf, UserMetadataItem } from 'src/types';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
@ -49,7 +50,7 @@ export class UserService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: Partial<UserEntity> = {
|
const update: Updateable<UserTable> = {
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
};
|
};
|
||||||
@ -229,7 +230,7 @@ export class UserService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean {
|
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean {
|
||||||
if (!user.deletedAt) {
|
if (!user.deletedAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
1
server/test/fixtures/asset.stub.ts
vendored
1
server/test/fixtures/asset.stub.ts
vendored
@ -41,7 +41,6 @@ export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity =
|
|||||||
return {
|
return {
|
||||||
id: stackId,
|
id: stackId,
|
||||||
assets,
|
assets,
|
||||||
owner: assets[0].owner,
|
|
||||||
ownerId: assets[0].ownerId,
|
ownerId: assets[0].ownerId,
|
||||||
primaryAsset: assets[0],
|
primaryAsset: assets[0],
|
||||||
primaryAssetId: assets[0].id,
|
primaryAssetId: assets[0].id,
|
||||||
|
10
server/test/fixtures/person.stub.ts
vendored
10
server/test/fixtures/person.stub.ts
vendored
@ -7,7 +7,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||||
@ -22,7 +21,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||||
@ -37,7 +35,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: 'Person 1',
|
name: 'Person 1',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||||
@ -52,7 +49,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: 'Person 1',
|
name: 'Person 1',
|
||||||
birthDate: '1976-06-30',
|
birthDate: '1976-06-30',
|
||||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||||
@ -67,7 +63,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '',
|
thumbnailPath: '',
|
||||||
@ -82,7 +77,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/new/path/to/thumbnail.jpg',
|
thumbnailPath: '/new/path/to/thumbnail.jpg',
|
||||||
@ -97,7 +91,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: 'Person 1',
|
name: 'Person 1',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail',
|
thumbnailPath: '/path/to/thumbnail',
|
||||||
@ -112,7 +105,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: 'Person 2',
|
name: 'Person 2',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail',
|
thumbnailPath: '/path/to/thumbnail',
|
||||||
@ -127,7 +119,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail',
|
thumbnailPath: '/path/to/thumbnail',
|
||||||
@ -142,7 +133,6 @@ export const personStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
ownerId: userStub.admin.id,
|
ownerId: userStub.admin.id,
|
||||||
owner: userStub.admin,
|
|
||||||
name: 'Person 1',
|
name: 'Person 1',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||||
|
7
server/test/fixtures/shared-link.stub.ts
vendored
7
server/test/fixtures/shared-link.stub.ts
vendored
@ -1,10 +1,10 @@
|
|||||||
|
import { UserAdmin } from 'src/database';
|
||||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { ExifResponseDto } from 'src/dtos/exif.dto';
|
import { ExifResponseDto } from 'src/dtos/exif.dto';
|
||||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
||||||
import { mapUser } from 'src/dtos/user.dto';
|
import { mapUser } from 'src/dtos/user.dto';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
|
||||||
import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
|
import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
@ -106,7 +106,6 @@ export const sharedLinkStub = {
|
|||||||
individual: Object.freeze({
|
individual: Object.freeze({
|
||||||
id: '123',
|
id: '123',
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
user: userStub.admin,
|
|
||||||
key: sharedLinkBytes,
|
key: sharedLinkBytes,
|
||||||
type: SharedLinkType.INDIVIDUAL,
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
@ -154,7 +153,6 @@ export const sharedLinkStub = {
|
|||||||
readonlyNoExif: Object.freeze<SharedLinkEntity>({
|
readonlyNoExif: Object.freeze<SharedLinkEntity>({
|
||||||
id: '123',
|
id: '123',
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
user: userStub.admin,
|
|
||||||
key: sharedLinkBytes,
|
key: sharedLinkBytes,
|
||||||
type: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
@ -185,7 +183,7 @@ export const sharedLinkStub = {
|
|||||||
{
|
{
|
||||||
id: 'id_1',
|
id: 'id_1',
|
||||||
status: AssetStatus.ACTIVE,
|
status: AssetStatus.ACTIVE,
|
||||||
owner: undefined as unknown as UserEntity,
|
owner: undefined as unknown as UserAdmin,
|
||||||
ownerId: 'user_id_1',
|
ownerId: 'user_id_1',
|
||||||
deviceAssetId: 'device_asset_id_1',
|
deviceAssetId: 'device_asset_id_1',
|
||||||
deviceId: 'device_id_1',
|
deviceId: 'device_id_1',
|
||||||
@ -253,7 +251,6 @@ export const sharedLinkStub = {
|
|||||||
passwordRequired: Object.freeze<SharedLinkEntity>({
|
passwordRequired: Object.freeze<SharedLinkEntity>({
|
||||||
id: '123',
|
id: '123',
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
user: userStub.admin,
|
|
||||||
key: sharedLinkBytes,
|
key: sharedLinkBytes,
|
||||||
type: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
|
32
server/test/fixtures/user.stub.ts
vendored
32
server/test/fixtures/user.stub.ts
vendored
@ -1,13 +1,12 @@
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
|
||||||
export const userStub = {
|
export const userStub = {
|
||||||
admin: Object.freeze<UserEntity>({
|
admin: <UserAdmin>{
|
||||||
...authStub.admin.user,
|
...authStub.admin.user,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
profileChangedAt: new Date('2021-01-01'),
|
||||||
password: 'admin_password',
|
|
||||||
name: 'admin_name',
|
name: 'admin_name',
|
||||||
id: 'admin_id',
|
id: 'admin_id',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
@ -17,16 +16,14 @@ export const userStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
assets: [],
|
|
||||||
metadata: [],
|
metadata: [],
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
}),
|
},
|
||||||
user1: Object.freeze<UserEntity>({
|
user1: <UserAdmin>{
|
||||||
...authStub.user1.user,
|
...authStub.user1.user,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
profileChangedAt: new Date('2021-01-01'),
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
@ -35,7 +32,6 @@ export const userStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
assets: [],
|
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
key: UserMetadataKey.PREFERENCES,
|
key: UserMetadataKey.PREFERENCES,
|
||||||
@ -44,13 +40,12 @@ export const userStub = {
|
|||||||
],
|
],
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
}),
|
},
|
||||||
user2: Object.freeze<UserEntity>({
|
user2: <UserAdmin>{
|
||||||
...authStub.user2.user,
|
...authStub.user2.user,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
profileChangedAt: new Date('2021-01-01'),
|
||||||
metadata: [],
|
metadata: [],
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
@ -59,16 +54,14 @@ export const userStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
assets: [],
|
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
}),
|
},
|
||||||
storageLabel: Object.freeze<UserEntity>({
|
storageLabel: <UserAdmin>{
|
||||||
...authStub.user1.user,
|
...authStub.user1.user,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
profileChangedAt: new Date('2021-01-01'),
|
||||||
metadata: [],
|
metadata: [],
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
@ -77,16 +70,14 @@ export const userStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
assets: [],
|
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
}),
|
},
|
||||||
profilePath: Object.freeze<UserEntity>({
|
profilePath: <UserAdmin>{
|
||||||
...authStub.user1.user,
|
...authStub.user1.user,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
profileChangedAt: new Date('2021-01-01'),
|
||||||
metadata: [],
|
metadata: [],
|
||||||
password: 'immich_password',
|
|
||||||
name: 'immich_name',
|
name: 'immich_name',
|
||||||
storageLabel: 'label-1',
|
storageLabel: 'label-1',
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
@ -95,8 +86,7 @@ export const userStub = {
|
|||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
assets: [],
|
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
}),
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user