1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(server,web): add force delete to immediately remove user (#7681)

* feat(server,web): add force delete to immediately remove user

* update wording on force delete confirmation

* fix force delete css

* PR feedback

* cleanup user service delete for force

* adding user status column

* some cleanup and tests

* more test fixes

* run npm run sql:generate

* chore: cleanup and websocket

* chore: linting

* userRepository.restore

* removed bad color class from delete-confirm-dialoge

* additional confirmation for user force delete

* shorten confirmation message

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Sam Holton 2024-03-08 17:49:39 -05:00 committed by GitHub
parent 9cb0a1ffbf
commit 7a4ae7d142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 300 additions and 86 deletions

View file

@ -76,6 +76,7 @@ export const signupResponseDto = {
memoriesEnabled: true, memoriesEnabled: true,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
quotaSizeInBytes: null, quotaSizeInBytes: null,
status: 'active',
}, },
}; };

View file

@ -58,6 +58,7 @@ doc/CreateTagDto.md
doc/CreateUserDto.md doc/CreateUserDto.md
doc/CuratedLocationsResponseDto.md doc/CuratedLocationsResponseDto.md
doc/CuratedObjectsResponseDto.md doc/CuratedObjectsResponseDto.md
doc/DeleteUserDto.md
doc/DownloadApi.md doc/DownloadApi.md
doc/DownloadArchiveInfo.md doc/DownloadArchiveInfo.md
doc/DownloadInfoDto.md doc/DownloadInfoDto.md
@ -184,6 +185,7 @@ doc/UserApi.md
doc/UserAvatarColor.md doc/UserAvatarColor.md
doc/UserDto.md doc/UserDto.md
doc/UserResponseDto.md doc/UserResponseDto.md
doc/UserStatus.md
doc/ValidateAccessTokenResponseDto.md doc/ValidateAccessTokenResponseDto.md
doc/ValidateLibraryDto.md doc/ValidateLibraryDto.md
doc/ValidateLibraryImportPathResponseDto.md doc/ValidateLibraryImportPathResponseDto.md
@ -268,6 +270,7 @@ lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart lib/model/create_user_dto.dart
lib/model/curated_locations_response_dto.dart lib/model/curated_locations_response_dto.dart
lib/model/curated_objects_response_dto.dart lib/model/curated_objects_response_dto.dart
lib/model/delete_user_dto.dart
lib/model/download_archive_info.dart lib/model/download_archive_info.dart
lib/model/download_info_dto.dart lib/model/download_info_dto.dart
lib/model/download_response_dto.dart lib/model/download_response_dto.dart
@ -380,6 +383,7 @@ lib/model/usage_by_user_dto.dart
lib/model/user_avatar_color.dart lib/model/user_avatar_color.dart
lib/model/user_dto.dart lib/model/user_dto.dart
lib/model/user_response_dto.dart lib/model/user_response_dto.dart
lib/model/user_status.dart
lib/model/validate_access_token_response_dto.dart lib/model/validate_access_token_response_dto.dart
lib/model/validate_library_dto.dart lib/model/validate_library_dto.dart
lib/model/validate_library_import_path_response_dto.dart lib/model/validate_library_import_path_response_dto.dart
@ -441,6 +445,7 @@ test/create_tag_dto_test.dart
test/create_user_dto_test.dart test/create_user_dto_test.dart
test/curated_locations_response_dto_test.dart test/curated_locations_response_dto_test.dart
test/curated_objects_response_dto_test.dart test/curated_objects_response_dto_test.dart
test/delete_user_dto_test.dart
test/download_api_test.dart test/download_api_test.dart
test/download_archive_info_test.dart test/download_archive_info_test.dart
test/download_info_dto_test.dart test/download_info_dto_test.dart
@ -567,6 +572,7 @@ test/user_api_test.dart
test/user_avatar_color_test.dart test/user_avatar_color_test.dart
test/user_dto_test.dart test/user_dto_test.dart
test/user_response_dto_test.dart test/user_response_dto_test.dart
test/user_status_test.dart
test/validate_access_token_response_dto_test.dart test/validate_access_token_response_dto_test.dart
test/validate_library_dto_test.dart test/validate_library_dto_test.dart
test/validate_library_import_path_response_dto_test.dart test/validate_library_import_path_response_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

BIN
mobile/openapi/doc/DeleteUserDto.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/UserStatus.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/lib/model/user_status.dart generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/test/user_status_test.dart generated Normal file

Binary file not shown.

View file

@ -6402,6 +6402,16 @@
} }
} }
], ],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteUserDto"
}
}
},
"required": true
},
"responses": { "responses": {
"200": { "200": {
"content": { "content": {
@ -7750,6 +7760,14 @@
], ],
"type": "object" "type": "object"
}, },
"DeleteUserDto": {
"properties": {
"force": {
"type": "boolean"
}
},
"type": "object"
},
"DownloadArchiveInfo": { "DownloadArchiveInfo": {
"properties": { "properties": {
"assetIds": { "assetIds": {
@ -8616,6 +8634,9 @@
"shouldChangePassword": { "shouldChangePassword": {
"type": "boolean" "type": "boolean"
}, },
"status": {
"$ref": "#/components/schemas/UserStatus"
},
"storageLabel": { "storageLabel": {
"nullable": true, "nullable": true,
"type": "string" "type": "string"
@ -8638,6 +8659,7 @@
"quotaSizeInBytes", "quotaSizeInBytes",
"quotaUsageInBytes", "quotaUsageInBytes",
"shouldChangePassword", "shouldChangePassword",
"status",
"storageLabel", "storageLabel",
"updatedAt" "updatedAt"
], ],
@ -10561,6 +10583,9 @@
"shouldChangePassword": { "shouldChangePassword": {
"type": "boolean" "type": "boolean"
}, },
"status": {
"$ref": "#/components/schemas/UserStatus"
},
"storageLabel": { "storageLabel": {
"nullable": true, "nullable": true,
"type": "string" "type": "string"
@ -10583,11 +10608,20 @@
"quotaSizeInBytes", "quotaSizeInBytes",
"quotaUsageInBytes", "quotaUsageInBytes",
"shouldChangePassword", "shouldChangePassword",
"status",
"storageLabel", "storageLabel",
"updatedAt" "updatedAt"
], ],
"type": "object" "type": "object"
}, },
"UserStatus": {
"enum": [
"active",
"removing",
"deleted"
],
"type": "string"
},
"ValidateAccessTokenResponseDto": { "ValidateAccessTokenResponseDto": {
"properties": { "properties": {
"authStatus": { "authStatus": {

View file

@ -75,6 +75,7 @@ export type UserResponseDto = {
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null; quotaUsageInBytes: number | null;
shouldChangePassword: boolean; shouldChangePassword: boolean;
status: UserStatus;
storageLabel: string | null; storageLabel: string | null;
updatedAt: string; updatedAt: string;
}; };
@ -518,6 +519,7 @@ export type PartnerResponseDto = {
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null; quotaUsageInBytes: number | null;
shouldChangePassword: boolean; shouldChangePassword: boolean;
status: UserStatus;
storageLabel: string | null; storageLabel: string | null;
updatedAt: string; updatedAt: string;
}; };
@ -994,6 +996,9 @@ export type CreateProfileImageResponseDto = {
profileImagePath: string; profileImagePath: string;
userId: string; userId: string;
}; };
export type DeleteUserDto = {
force?: boolean;
};
export function getActivities({ albumId, assetId, level, $type, userId }: { export function getActivities({ albumId, assetId, level, $type, userId }: {
albumId: string; albumId: string;
assetId?: string; assetId?: string;
@ -2678,16 +2683,18 @@ export function getProfileImage({ id }: {
...opts ...opts
})); }));
} }
export function deleteUser({ id }: { export function deleteUser({ id, deleteUserDto }: {
id: string; id: string;
deleteUserDto: DeleteUserDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: UserResponseDto; data: UserResponseDto;
}>(`/user/${encodeURIComponent(id)}`, { }>(`/user/${encodeURIComponent(id)}`, oazapfts.json({
...opts, ...opts,
method: "DELETE" method: "DELETE",
})); body: deleteUserDto
})));
} }
export function restoreUser({ id }: { export function restoreUser({ id }: {
id: string; id: string;
@ -2724,6 +2731,11 @@ export enum UserAvatarColor {
Gray = "gray", Gray = "gray",
Amber = "amber" Amber = "amber"
} }
export enum UserStatus {
Active = "active",
Removing = "removing",
Deleted = "deleted"
}
export enum TagTypeEnum { export enum TagTypeEnum {
Object = "OBJECT", Object = "OBJECT",
Face = "FACE", Face = "FACE",

View file

@ -280,6 +280,11 @@ export class JobService {
} }
break; break;
} }
case JobName.USER_DELETION: {
this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id);
break;
}
} }
} }
} }

View file

@ -4,6 +4,7 @@ export const ICommunicationRepository = 'ICommunicationRepository';
export enum ClientEvent { export enum ClientEvent {
UPLOAD_SUCCESS = 'on_upload_success', UPLOAD_SUCCESS = 'on_upload_success',
USER_DELETE = 'on_user_delete',
ASSET_DELETE = 'on_asset_delete', ASSET_DELETE = 'on_asset_delete',
ASSET_TRASH = 'on_asset_trash', ASSET_TRASH = 'on_asset_trash',
ASSET_UPDATE = 'on_asset_update', ASSET_UPDATE = 'on_asset_update',
@ -22,6 +23,7 @@ export enum ServerEvent {
export interface ClientEventMap { export interface ClientEventMap {
[ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto;
[ClientEvent.USER_DELETE]: string;
[ClientEvent.ASSET_DELETE]: string; [ClientEvent.ASSET_DELETE]: string;
[ClientEvent.ASSET_TRASH]: string[]; [ClientEvent.ASSET_TRASH]: string[];
[ClientEvent.ASSET_UPDATE]: AssetResponseDto; [ClientEvent.ASSET_UPDATE]: AssetResponseDto;

View file

@ -32,7 +32,6 @@ export interface IUserRepository {
create(user: Partial<UserEntity>): Promise<UserEntity>; create(user: Partial<UserEntity>): Promise<UserEntity>;
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>; update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>; delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
restore(user: UserEntity): Promise<UserEntity>;
updateUsage(id: string, delta: number): Promise<void>; updateUsage(id: string, delta: number): Promise<void>;
syncUsage(id?: string): Promise<void>; syncUsage(id?: string): Promise<void>;
} }

View file

@ -0,0 +1,6 @@
import { ValidateBoolean } from '../../domain.util';
export class DeleteUserDto {
@ValidateBoolean({ optional: true })
force?: boolean;
}

View file

@ -1,3 +1,4 @@
export * from './create-profile-image.dto'; export * from './create-profile-image.dto';
export * from './create-user.dto'; export * from './create-user.dto';
export * from './delete-user.dto';
export * from './update-user.dto'; export * from './update-user.dto';

View file

@ -1,4 +1,4 @@
import { UserAvatarColor, UserEntity } from '@app/infra/entities'; import { UserAvatarColor, UserEntity, UserStatus } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator'; import { IsEnum } from 'class-validator';
@ -33,6 +33,8 @@ export class UserResponseDto extends UserDto {
quotaSizeInBytes!: number | null; quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaUsageInBytes!: number | null; quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
status!: string;
} }
export const mapSimpleUser = (entity: UserEntity): UserDto => { export const mapSimpleUser = (entity: UserEntity): UserDto => {
@ -58,5 +60,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
memoriesEnabled: entity.memoriesEnabled, memoriesEnabled: entity.memoriesEnabled,
quotaSizeInBytes: entity.quotaSizeInBytes, quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes, quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
}; };
} }

View file

@ -1,4 +1,4 @@
import { UserEntity } from '@app/infra/entities'; import { UserEntity, UserStatus } from '@app/infra/entities';
import { import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
@ -243,16 +243,14 @@ describe(UserService.name, () => {
it('should throw error if user could not be found', async () => { it('should throw error if user could not be found', async () => {
when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(null); when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(null);
await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException); await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
expect(userMock.restore).not.toHaveBeenCalled(); expect(userMock.update).not.toHaveBeenCalled();
}); });
it('should restore an user', async () => { it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: true }); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1);
}); });
}); });
@ -260,27 +258,47 @@ describe(UserService.name, () => {
it('should throw error if user could not be found', async () => { it('should throw error if user could not be found', async () => {
userMock.get.mockResolvedValue(null); userMock.get.mockResolvedValue(null);
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException); await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException);
expect(userMock.delete).not.toHaveBeenCalled(); expect(userMock.delete).not.toHaveBeenCalled();
}); });
it('cannot delete admin user', async () => { it('cannot delete admin user', async () => {
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('should require the auth user be an admin', async () => { it('should require the auth user be an admin', async () => {
await expect(sut.delete(authStub.user1, authStub.admin.user.id)).rejects.toBeInstanceOf(ForbiddenException); await expect(sut.delete(authStub.user1, authStub.admin.user.id, {})).rejects.toBeInstanceOf(ForbiddenException);
expect(userMock.delete).not.toHaveBeenCalled(); expect(userMock.delete).not.toHaveBeenCalled();
}); });
it('should delete user', async () => { it('should delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.delete.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {}); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
expect(userMock.delete).toHaveBeenCalledWith(userStub.user1); status: UserStatus.DELETED,
deletedAt: expect.any(Date),
});
});
it('should force delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id, { force: true })).resolves.toEqual(
mapUser(userStub.user1),
);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.REMOVING,
deletedAt: expect.any(Date),
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.USER_DELETION,
data: { id: userStub.user1.id, force: true },
});
}); });
}); });

View file

@ -1,4 +1,4 @@
import { UserEntity } from '@app/infra/entities'; import { UserEntity, UserStatus } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger'; import { ImmichLogger } from '@app/infra/logger';
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -18,7 +18,7 @@ import {
} from '../repositories'; } from '../repositories';
import { StorageCore, StorageFolder } from '../storage'; import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config/system-config.core'; import { SystemConfigCore } from '../system-config/system-config.core';
import { CreateUserDto, UpdateUserDto } from './dto'; import { CreateUserDto, DeleteUserDto, UpdateUserDto } from './dto';
import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto'; import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto';
import { UserCore } from './user.core'; import { UserCore } from './user.core';
@ -73,22 +73,29 @@ export class UserService {
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser); return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
} }
async delete(auth: AuthDto, id: string): Promise<UserResponseDto> { async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
const user = await this.findOrFail(id, {}); const { force } = dto;
if (user.isAdmin) { const { isAdmin } = await this.findOrFail(id, {});
if (isAdmin) {
throw new ForbiddenException('Cannot delete admin user'); throw new ForbiddenException('Cannot delete admin user');
} }
await this.albumRepository.softDeleteAll(id); await this.albumRepository.softDeleteAll(id);
return this.userRepository.delete(user).then(mapUser); const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
if (force) {
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
}
return mapUser(user);
} }
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> { async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
let user = await this.findOrFail(id, { withDeleted: true }); await this.findOrFail(id, { withDeleted: true });
user = await this.userRepository.restore(user);
await this.albumRepository.restoreAll(id); await this.albumRepository.restoreAll(id);
return mapUser(user); return this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }).then(mapUser);
} }
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> { async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
@ -154,7 +161,7 @@ export class UserService {
return true; return true;
} }
async handleUserDelete({ id }: IEntityJob) { async handleUserDelete({ id, force }: IEntityJob) {
const config = await this.configCore.getConfig(); const config = await this.configCore.getConfig();
const user = await this.userRepository.get(id, { withDeleted: true }); const user = await this.userRepository.get(id, { withDeleted: true });
if (!user) { if (!user) {
@ -162,7 +169,7 @@ export class UserService {
} }
// just for extra protection here // just for extra protection here
if (!this.isReadyForDeletion(user, config.user.deleteDelay)) { if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`); this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
return false; return false;
} }

View file

@ -3,6 +3,7 @@ import {
CreateUserDto as CreateDto, CreateUserDto as CreateDto,
CreateProfileImageDto, CreateProfileImageDto,
CreateProfileImageResponseDto, CreateProfileImageResponseDto,
DeleteUserDto,
UpdateUserDto as UpdateDto, UpdateUserDto as UpdateDto,
UserResponseDto, UserResponseDto,
UserService, UserService,
@ -66,8 +67,12 @@ export class UserController {
@AdminRoute() @AdminRoute()
@Delete(':id') @Delete(':id')
deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> { deleteUser(
return this.service.delete(auth, id); @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: DeleteUserDto,
): Promise<UserResponseDto> {
return this.service.delete(auth, id, dto);
} }
@AdminRoute() @AdminRoute()

View file

@ -23,6 +23,12 @@ export enum UserAvatarColor {
AMBER = 'amber', AMBER = 'amber',
} }
export enum UserStatus {
ACTIVE = 'active',
REMOVING = 'removing',
DELETED = 'deleted',
}
@Entity('users') @Entity('users')
export class UserEntity { export class UserEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -61,6 +67,9 @@ export class UserEntity {
@DeleteDateColumn({ type: 'timestamptz' }) @DeleteDateColumn({ type: 'timestamptz' })
deletedAt!: Date | null; deletedAt!: Date | null;
@Column({ type: 'varchar', default: UserStatus.ACTIVE })
status!: UserStatus;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserStatus1709870213078 implements MigrationInterface {
name = 'AddUserStatus1709870213078'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "status" character varying NOT NULL DEFAULT 'active'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "status"`);
}
}

View file

@ -77,10 +77,6 @@ export class UserRepository implements IUserRepository {
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user); return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
} }
async restore(user: UserEntity): Promise<UserEntity> {
return this.userRepository.recover(user);
}
@GenerateSql() @GenerateSql()
async getUserStats(): Promise<UserStatsQueryResponse[]> { async getUserStats(): Promise<UserStatsQueryResponse[]> {
const stats = await this.userRepository const stats = await this.userRepository
@ -135,6 +131,6 @@ export class UserRepository implements IUserRepository {
private async save(user: Partial<UserEntity>) { private async save(user: Partial<UserEntity>) {
const { id } = await this.userRepository.save(user); const { id } = await this.userRepository.save(user);
return this.userRepository.findOneByOrFail({ id }); return this.userRepository.findOneOrFail({ where: { id }, withDeleted: true });
} }
} }

View file

@ -26,6 +26,7 @@ FROM
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -41,6 +42,7 @@ FROM
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -100,6 +102,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -115,6 +118,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -156,6 +160,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -171,6 +176,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -284,6 +290,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -311,6 +318,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -355,6 +363,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -382,6 +391,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -463,6 +473,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -490,6 +501,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
@ -552,6 +564,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",

View file

@ -20,6 +20,7 @@ FROM
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword", "APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
"APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt", "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
"APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt", "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
"APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status",
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
"APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled", "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled",
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",

View file

@ -28,6 +28,7 @@ FROM
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
@ -143,6 +144,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
@ -188,6 +190,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
@ -227,6 +230,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled", "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",

View file

@ -155,6 +155,7 @@ FROM
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
@ -258,6 +259,7 @@ SELECT
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
@ -311,6 +313,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword", "SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
"SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt", "SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
"SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt", "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
"SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status",
"SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
"SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled", "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled",
"SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes", "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",

View file

@ -13,6 +13,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
@ -59,6 +60,7 @@ SELECT
"user"."shouldChangePassword" AS "user_shouldChangePassword", "user"."shouldChangePassword" AS "user_shouldChangePassword",
"user"."createdAt" AS "user_createdAt", "user"."createdAt" AS "user_createdAt",
"user"."deletedAt" AS "user_deletedAt", "user"."deletedAt" AS "user_deletedAt",
"user"."status" AS "user_status",
"user"."updatedAt" AS "user_updatedAt", "user"."updatedAt" AS "user_updatedAt",
"user"."memoriesEnabled" AS "user_memoriesEnabled", "user"."memoriesEnabled" AS "user_memoriesEnabled",
"user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
@ -82,6 +84,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
@ -107,6 +110,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt", "UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt", "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled", "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",

View file

@ -23,6 +23,7 @@ FROM
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword", "UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
"UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt", "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
"UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt", "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
"UserTokenEntity__UserTokenEntity_user"."status" AS "UserTokenEntity__UserTokenEntity_user_status",
"UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt", "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
"UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled", "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled",
"UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes", "UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes",

View file

@ -17,7 +17,6 @@ export const newUserRepositoryMock = (reset = true): jest.Mocked<IUserRepository
update: jest.fn(), update: jest.fn(),
delete: jest.fn(), delete: jest.fn(),
getDeletedUsers: jest.fn(), getDeletedUsers: jest.fn(),
restore: jest.fn(),
hasAdmin: jest.fn(), hasAdmin: jest.fn(),
updateUsage: jest.fn(), updateUsage: jest.fn(),
syncUsage: jest.fn(), syncUsage: jest.fn(),

View file

@ -7,6 +7,10 @@
export let user: UserResponseDto; export let user: UserResponseDto;
let forceDelete = false;
let deleteButtonDisabled = false;
let userIdInput: string = '';
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
success: void; success: void;
fail: void; fail: void;
@ -15,7 +19,11 @@
const handleDeleteUser = async () => { const handleDeleteUser = async () => {
try { try {
const { deletedAt } = await deleteUser({ id: user.id }); const { deletedAt } = await deleteUser({
id: user.id,
deleteUserDto: { force: forceDelete },
});
if (deletedAt == undefined) { if (deletedAt == undefined) {
dispatch('fail'); dispatch('fail');
} else { } else {
@ -26,20 +34,68 @@
dispatch('fail'); dispatch('fail');
} }
}; };
const handleConfirm = (e: Event) => {
userIdInput = (e.target as HTMLInputElement).value;
deleteButtonDisabled = userIdInput != user.email;
};
</script> </script>
<ConfirmDialogue <ConfirmDialogue
title="Delete User" title="Delete User"
confirmText="Delete" confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
onConfirm={handleDeleteUser} onConfirm={handleDeleteUser}
onClose={() => dispatch('cancel')} onClose={() => dispatch('cancel')}
disabled={deleteButtonDisabled}
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p> {#if forceDelete}
<b>{user.name}</b>'s account and assets will be permanently deleted after {$serverConfig.userDeleteDelay} days. <p>
</p> <b>{user.name}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.
<p>Are you sure you want to continue?</p> </p>
{:else}
<p>
<b>{user.name}</b>'s account and assets will be scheduled for permanent deletion in {$serverConfig.userDeleteDelay}
days.
</p>
{/if}
<div class="flex justify-center m-4 gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="forceDelete">
Queue user and assets for immediate deletion
</label>
<input
id="forceDelete"
type="checkbox"
class="form-checkbox h-5 w-5"
bind:checked={forceDelete}
on:change={() => {
deleteButtonDisabled = forceDelete;
}}
/>
</div>
{#if forceDelete}
<p class="text-immich-error">
WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be
recovered.
</p>
<p class="immich-form-label text-sm" id="confirm-user-desc">
To confirm, type "{user.email}" below
</p>
<input
class="immich-form-input w-full pb-2"
id="confirm-user-id"
aria-describedby="confirm-user-desc"
name="confirm-user-id"
type="text"
on:input={handleConfirm}
/>
{/if}
</div> </div>
</svelte:fragment> </svelte:fragment>
</ConfirmDialogue> </ConfirmDialogue>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { handleError } from '$lib/utils/handle-error';
import { restoreUser, type UserResponseDto } from '@immich/sdk'; import { restoreUser, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -12,10 +13,15 @@
}>(); }>();
const handleRestoreUser = async () => { const handleRestoreUser = async () => {
const { deletedAt } = await restoreUser({ id: user.id }); try {
if (deletedAt == undefined) { const { deletedAt } = await restoreUser({ id: user.id });
dispatch('success'); if (deletedAt == undefined) {
} else { dispatch('success');
} else {
dispatch('fail');
}
} catch (error) {
handleError(error, 'Unable to restore user');
dispatch('fail'); dispatch('fail');
} }
}; };

View file

@ -12,6 +12,7 @@ export interface ReleaseEvent {
} }
export interface Events { export interface Events {
on_upload_success: (asset: AssetResponseDto) => void; on_upload_success: (asset: AssetResponseDto) => void;
on_user_delete: (id: string) => void;
on_asset_delete: (assetId: string) => void; on_asset_delete: (assetId: string) => void;
on_asset_trash: (assetIds: string[]) => void; on_asset_trash: (assetIds: string[]) => void;
on_asset_update: (asset: AssetResponseDto) => void; on_asset_update: (asset: AssetResponseDto) => void;

View file

@ -8,11 +8,18 @@
import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { serverConfig } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
import { asByteUnitString } from '$lib/utils/byte-units'; import { asByteUnitString } from '$lib/utils/byte-units';
import { getAllUsers, type UserResponseDto } from '@immich/sdk'; import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
import { DateTime } from 'luxon';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -26,13 +33,26 @@
let shouldShowRestoreDialog = false; let shouldShowRestoreDialog = false;
let selectedUser: UserResponseDto; let selectedUser: UserResponseDto;
const refresh = async () => {
allUsers = await getAllUsers({ isAll: false });
};
const onDeleteSuccess = (userId: string) => {
const user = allUsers.find(({ id }) => id === userId);
if (user) {
allUsers = allUsers.filter((user) => user.id !== userId);
notificationController.show({
type: NotificationType.Info,
message: `User ${user.email} has been successfully removed.`,
});
}
};
onMount(() => { onMount(() => {
allUsers = $page.data.allUsers; allUsers = $page.data.allUsers;
});
const isDeleted = (user: UserResponseDto): boolean => { return websocketEvents.on('on_user_delete', onDeleteSuccess);
return user.deletedAt != undefined; });
};
const deleteDateFormat: Intl.DateTimeFormatOptions = { const deleteDateFormat: Intl.DateTimeFormatOptions = {
month: 'long', month: 'long',
@ -40,14 +60,14 @@
year: 'numeric', year: 'numeric',
}; };
const getDeleteDate = (user: UserResponseDto): string => { const getDeleteDate = (deletedAt: string): string => {
let deletedAt = new Date(user.deletedAt ?? Date.now()); return DateTime.fromISO(deletedAt)
deletedAt.setDate(deletedAt.getDate() + 7); .plus({ days: $serverConfig.userDeleteDelay })
return deletedAt.toLocaleString($locale, deleteDateFormat); .toLocaleString(deleteDateFormat, { locale: $locale });
}; };
const onUserCreated = async () => { const onUserCreated = async () => {
allUsers = await getAllUsers({ isAll: false }); await refresh();
shouldShowCreateUserForm = false; shouldShowCreateUserForm = false;
}; };
@ -57,12 +77,12 @@
}; };
const onEditUserSuccess = async () => { const onEditUserSuccess = async () => {
allUsers = await getAllUsers({ isAll: false }); await refresh();
shouldShowEditUserForm = false; shouldShowEditUserForm = false;
}; };
const onEditPasswordSuccess = async () => { const onEditPasswordSuccess = async () => {
allUsers = await getAllUsers({ isAll: false }); await refresh();
shouldShowEditUserForm = false; shouldShowEditUserForm = false;
shouldShowInfoPanel = true; shouldShowInfoPanel = true;
}; };
@ -72,13 +92,8 @@
shouldShowDeleteConfirmDialog = true; shouldShowDeleteConfirmDialog = true;
}; };
const onUserDeleteSuccess = async () => { const onUserDelete = async () => {
allUsers = await getAllUsers({ isAll: false }); await refresh();
shouldShowDeleteConfirmDialog = false;
};
const onUserDeleteFail = async () => {
allUsers = await getAllUsers({ isAll: false });
shouldShowDeleteConfirmDialog = false; shouldShowDeleteConfirmDialog = false;
}; };
@ -87,14 +102,8 @@
shouldShowRestoreDialog = true; shouldShowRestoreDialog = true;
}; };
const onUserRestoreSuccess = async () => { const onUserRestore = async () => {
allUsers = await getAllUsers({ isAll: false }); await refresh();
shouldShowRestoreDialog = false;
};
const onUserRestoreFail = async () => {
// show fail dialog
allUsers = await getAllUsers({ isAll: false });
shouldShowRestoreDialog = false; shouldShowRestoreDialog = false;
}; };
</script> </script>
@ -123,8 +132,8 @@
{#if shouldShowDeleteConfirmDialog} {#if shouldShowDeleteConfirmDialog}
<DeleteConfirmDialog <DeleteConfirmDialog
user={selectedUser} user={selectedUser}
on:success={onUserDeleteSuccess} on:success={onUserDelete}
on:fail={onUserDeleteFail} on:fail={onUserDelete}
on:cancel={() => (shouldShowDeleteConfirmDialog = false)} on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
/> />
{/if} {/if}
@ -132,8 +141,8 @@
{#if shouldShowRestoreDialog} {#if shouldShowRestoreDialog}
<RestoreDialogue <RestoreDialogue
user={selectedUser} user={selectedUser}
on:success={onUserRestoreSuccess} on:success={onUserRestore}
on:fail={onUserRestoreFail} on:fail={onUserRestore}
on:cancel={() => (shouldShowRestoreDialog = false)} on:cancel={() => (shouldShowRestoreDialog = false)}
/> />
{/if} {/if}
@ -179,9 +188,7 @@
{#if allUsers} {#if allUsers}
{#each allUsers as immichUser, index} {#each allUsers as immichUser, index}
<tr <tr
class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {isDeleted( class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {immichUser.deletedAt
immichUser,
)
? 'bg-red-300 dark:bg-red-900' ? 'bg-red-300 dark:bg-red-900'
: index % 2 == 0 : index % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75' ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
@ -201,7 +208,7 @@
</div> </div>
</td> </td>
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm"> <td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm">
{#if !isDeleted(immichUser)} {#if !immichUser.deletedAt}
<button <button
on:click={() => editUserHandler(immichUser)} on:click={() => editUserHandler(immichUser)}
class="rounded-full bg-immich-primary p-2 sm:p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1" class="rounded-full bg-immich-primary p-2 sm:p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1"
@ -217,11 +224,11 @@
</button> </button>
{/if} {/if}
{/if} {/if}
{#if isDeleted(immichUser)} {#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
<button <button
on:click={() => restoreUserHandler(immichUser)} on:click={() => restoreUserHandler(immichUser)}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
title="scheduled removal on {getDeleteDate(immichUser)}" title="scheduled removal on {getDeleteDate(immichUser.deletedAt)}"
> >
<Icon path={mdiDeleteRestore} size="16" /> <Icon path={mdiDeleteRestore} size="16" />
</button> </button>

View file

@ -1,5 +1,5 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk'; import { UserAvatarColor, UserStatus, type UserResponseDto } from '@immich/sdk';
import { Sync } from 'factory.ts'; import { Sync } from 'factory.ts';
export const userFactory = Sync.makeFactory<UserResponseDto>({ export const userFactory = Sync.makeFactory<UserResponseDto>({
@ -18,4 +18,5 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
avatarColor: UserAvatarColor.Primary, avatarColor: UserAvatarColor.Primary,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
quotaSizeInBytes: null, quotaSizeInBytes: null,
status: UserStatus.Active,
}); });