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:
parent
9cb0a1ffbf
commit
7a4ae7d142
47 changed files with 300 additions and 86 deletions
|
@ -76,6 +76,7 @@ export const signupResponseDto = {
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
|
status: 'active',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
|
@ -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
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/DeleteUserDto.md
generated
Normal file
BIN
mobile/openapi/doc/DeleteUserDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
BIN
mobile/openapi/doc/PartnerResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserApi.md
generated
BIN
mobile/openapi/doc/UserApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserResponseDto.md
generated
BIN
mobile/openapi/doc/UserResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UserStatus.md
generated
Normal file
BIN
mobile/openapi/doc/UserStatus.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/user_api.dart
generated
BIN
mobile/openapi/lib/api/user_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/delete_user_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/delete_user_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_status.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_status.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/delete_user_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/delete_user_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
BIN
mobile/openapi/test/partner_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_api_test.dart
generated
BIN
mobile/openapi/test/user_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
BIN
mobile/openapi/test/user_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/user_status_test.dart
generated
Normal file
BIN
mobile/openapi/test/user_status_test.dart
generated
Normal file
Binary file not shown.
|
@ -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": {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -280,6 +280,11 @@ export class JobService {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case JobName.USER_DELETION: {
|
||||||
|
this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
6
server/src/domain/user/dto/delete-user.dto.ts
Normal file
6
server/src/domain/user/dto/delete-user.dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { ValidateBoolean } from '../../domain.util';
|
||||||
|
|
||||||
|
export class DeleteUserDto {
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
force?: boolean;
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
14
server/src/infra/migrations/1709870213078-AddUserStatus.ts
Normal file
14
server/src/infra/migrations/1709870213078-AddUserStatus.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue