From fe4b307fe6578220adb60b21d5880e50bda27df8 Mon Sep 17 00:00:00 2001 From: Zeeshan Khan Date: Mon, 7 Nov 2022 16:53:47 -0500 Subject: [PATCH] feat(server,web): Delete and restore user from the admin portal (#935) * delete and restore user from admin UI * addressed review comments and fix e2e test * added cron job to delete user, and some formatting changes * addressed review comments * adding missing queue registration --- mobile/openapi/README.md | Bin 9758 -> 9942 bytes mobile/openapi/doc/UserApi.md | Bin 9680 -> 12352 bytes mobile/openapi/doc/UserResponseDto.md | Bin 645 -> 695 bytes mobile/openapi/lib/api/user_api.dart | Bin 12989 -> 16120 bytes .../openapi/lib/model/user_response_dto.dart | Bin 5202 -> 5587 bytes .../user/response-dto/user-response.dto.ts | 2 + .../immich/src/api-v1/user/user-repository.ts | 24 ++- .../immich/src/api-v1/user/user.controller.ts | 15 ++ .../src/api-v1/user/user.service.spec.ts | 2 + .../immich/src/api-v1/user/user.service.ts | 48 +++++- .../background-task.processor.ts | 34 +---- .../schedule-tasks/schedule-tasks.module.ts | 11 +- .../schedule-tasks/schedule-tasks.service.ts | 20 +++ server/apps/immich/test/user.e2e-spec.ts | 2 + .../src/processors/user-deletion.processor.ts | 35 +++++ server/immich-openapi-specs.json | 2 +- server/libs/common/src/utils/asset-utils.ts | 39 +++++ server/libs/common/src/utils/index.ts | 2 + .../libs/common/src/utils/user-utils.spec.ts | 19 +++ server/libs/common/src/utils/user-utils.ts | 16 ++ .../libs/database/src/entities/user.entity.ts | 5 +- ...60744-AddingDeletedAtColumnInUserEntity.ts | 13 ++ .../job/src/constants/job-name.constant.ts | 5 + .../job/src/constants/queue-name.constant.ts | 1 + .../src/interfaces/user-deletion.interface.ts | 8 + web/src/api/open-api/api.ts | 140 ++++++++++++++++++ .../admin-page/delete-confirm-dialoge.svelte | 41 +++++ .../admin-page/restore-dialoge.svelte | 40 +++++ .../admin-page/user-management.svelte | 57 +++++-- web/src/routes/admin/+page.svelte | 69 ++++++++- 30 files changed, 594 insertions(+), 56 deletions(-) create mode 100644 server/apps/microservices/src/processors/user-deletion.processor.ts create mode 100644 server/libs/common/src/utils/asset-utils.ts create mode 100644 server/libs/common/src/utils/user-utils.spec.ts create mode 100644 server/libs/common/src/utils/user-utils.ts create mode 100644 server/libs/database/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts create mode 100644 server/libs/job/src/interfaces/user-deletion.interface.ts create mode 100644 web/src/lib/components/admin-page/delete-confirm-dialoge.svelte create mode 100644 web/src/lib/components/admin-page/restore-dialoge.svelte diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 9434ec63e5e751f19f60591e753efe872e059334..048a7627d1d8ff23c902becb35f8bf8459851e68 100644 GIT binary patch delta 160 zcmbQ|bIo_d196*_)ST3k)X?J8A}y_0jg|~H$@pDTM87=RH#wV(sFV2 zaSd_R(o)a|iA^?;l;8YVd_SXaQEG8Xei2S%pwb9)0{nwRV5aC-gTy^kYW1PYHup>R G^8f$@Bsj|e delta 17 ZcmccSJI`ms1M$rk5{DQ!M@iN2002rc2Pyyn diff --git a/mobile/openapi/doc/UserApi.md b/mobile/openapi/doc/UserApi.md index b5cc65c1e7855b64d5d6815c639fabd73358b3ad..e52857a2844e5e648a1679135bf7ccbffa053cac 100644 GIT binary patch delta 290 zcmccMeIQ{&HlssIYEEiNYG`q4k(O4h28ebn$kfYCQHDsA0tGY`Y814zTwHxzLtM4A z6!bx2`qd!PGo^NO3S$YQLQ!gQNq!MFV<0kbcFxSn(vGPhs)!{h)SzsdXLpKtEsl@?}$+Lpi}#f{`76ys1N Qfaam_p$2Z=CojYX0P<*Sf&c&j delta 27 jcmX?*aKU>+Hsj_^jJb@P^O!erZRQr17TzqXt<4GmoL~tq diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index d56b444ee0f8ace3e4811a364bd068db7802ee8b..99f2f5245b8500f304102ee45529642e75acdb17 100644 GIT binary patch delta 55 zcmZo=-Ojq<0i(8-R!VA4YDsE}V~LiQLXASSmX=FmNoq)DZmO16tOks&mz$zF`6lBU E00IgU)&Kwi delta 11 Scmdna+RD1&0pnyLrlkNHF9ak2 diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart index 52b8ae68b4e67796c0ab768386a1f467be3b9461..6af304115ae9ba836ad2a1e021e022c23582b1d5 100644 GIT binary patch delta 210 zcmdm+`lEJ(KR=I)tB-4ltAc)Maca?IJ;esDl+>KmlGIQTcc)_dW@#CBMmC6|$qC#W zCo8C!AuCB#F+t*PUZAp!X>$a>i>#D?a0tXu{c4cWo+-8ZMXALl`9-Od6{O3#p=_8n j0_ci1>nhnZPLAQ;3bGJIHPA+6KG4d!Oq*xxD6s(m4!le{ delta 17 YcmexSyEk=%KmX=bQD523*DVy;089c0CIA2c diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 740808ce2507fce5157d46772e6895310609e6c1..a6242d2c8411dc4e66c27c2fa6043c553fb2308c 100644 GIT binary patch delta 396 zcmcblaansqHq+#(i~^h~sX3`7sVR;nlhc_t3%ewiq=sbXrrImO#W#yGPi0h9P*dYl z0D=6HjMO4MxU7P$Et1kzEUJuJ3ff?m8YpV=N^^1)>=g_YtPsi-Cm&_~E~bMJuvLNU z*}R8s5@UU4ngWtBinaA`2Ax=?<&nnK(i!M@!>r#(ZKyr?vUP*pvNwS6}koGLj zw=gp>3@$0k%uCnMv<90{tB{&goQm5x0kC->JE7_~dvYYQ8Ym!KY^9K!SO5z&4Ultm h6p-DcqoA&?xp@)qDmHNi1tfRs0PU}}=Bnl50syTad^7+6 delta 47 zcmV+~0MP%_E7B;iYXXyq0; + get(userId: string, withDeleted?: boolean): Promise; getByEmail(email: string): Promise; getList(filter?: { excludeId?: string }): Promise; create(createUserDto: CreateUserDto): Promise; update(user: UserEntity, updateUserDto: UpdateUserDto): Promise; createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise; + delete(user: UserEntity): Promise; + restore(user: UserEntity): Promise; } export const USER_REPOSITORY = 'USER_REPOSITORY'; @@ -27,8 +29,8 @@ export class UserRepository implements IUserRepository { return bcrypt.hash(password, salt); } - async get(userId: string): Promise { - return this.userRepository.findOne({ where: { id: userId } }); + async get(userId: string, withDeleted?: boolean): Promise { + return this.userRepository.findOne({ where: { id: userId }, withDeleted: withDeleted }); } async getByEmail(email: string): Promise { @@ -40,9 +42,10 @@ export class UserRepository implements IUserRepository { if (!excludeId) { return this.userRepository.find(); // TODO: this should also be ordered the same as below } - - return this.userRepository.find({ + return this.userRepository + .find({ where: { id: Not(excludeId) }, + withDeleted: true, order: { createdAt: 'DESC', }, @@ -88,6 +91,17 @@ export class UserRepository implements IUserRepository { return this.userRepository.save(user); } + async delete(user: UserEntity): Promise { + if (user.isAdmin) { + throw new BadRequestException('Cannot delete admin user! stay sane!'); + } + return this.userRepository.softRemove(user); + } + + async restore(user: UserEntity): Promise { + return this.userRepository.recover(user); + } + async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise { user.profileImagePath = fileInfo.path; return this.userRepository.save(user); diff --git a/server/apps/immich/src/api-v1/user/user.controller.ts b/server/apps/immich/src/api-v1/user/user.controller.ts index aac54c4c54..0512a4af17 100644 --- a/server/apps/immich/src/api-v1/user/user.controller.ts +++ b/server/apps/immich/src/api-v1/user/user.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, + Delete, Body, Param, ValidationPipe, @@ -67,6 +68,20 @@ export class UserController { return await this.userService.getUserCount(); } + @Authenticated({ admin: true }) + @ApiBearerAuth() + @Delete('/:userId') + async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise { + return await this.userService.deleteUser(authUser, userId); + } + + @Authenticated({ admin: true }) + @ApiBearerAuth() + @Post('/:userId/restore') + async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise { + return await this.userService.restoreUser(authUser, userId); + } + @Authenticated() @ApiBearerAuth() @Put() diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index f60856dbe1..4fda062ca6 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -65,6 +65,8 @@ describe('UserService', () => { getByEmail: jest.fn(), getList: jest.fn(), update: jest.fn(), + delete: jest.fn(), + restore: jest.fn(), }; sui = new UserService(userRepositoryMock); diff --git a/server/apps/immich/src/api-v1/user/user.service.ts b/server/apps/immich/src/api-v1/user/user.service.ts index d62d56de23..b1b115ef26 100644 --- a/server/apps/immich/src/api-v1/user/user.service.ts +++ b/server/apps/immich/src/api-v1/user/user.service.ts @@ -1,11 +1,13 @@ import { BadRequestException, + ForbiddenException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException, StreamableFile, + UnauthorizedException, } from '@nestjs/common'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { CreateUserDto } from './dto/create-user.dto'; @@ -38,8 +40,8 @@ export class UserService { return allUserExceptRequestedUser.map(mapUser); } - async getUserById(userId: string): Promise { - const user = await this.userRepository.get(userId); + async getUserById(userId: string, withDeleted = false): Promise { + const user = await this.userRepository.get(userId, withDeleted); if (!user) { throw new NotFoundException('User not found'); } @@ -105,6 +107,48 @@ export class UserService { } } + async deleteUser(authUser: AuthUserDto, userId: string): Promise { + const requestor = await this.userRepository.get(authUser.id); + if (!requestor) { + throw new UnauthorizedException('Requestor not found'); + } + if (!requestor.isAdmin) { + throw new ForbiddenException('Unauthorized'); + } + const user = await this.userRepository.get(userId); + if (!user) { + throw new BadRequestException('User not found'); + } + try { + const deletedUser = await this.userRepository.delete(user); + return mapUser(deletedUser); + } catch (e) { + Logger.error(e, 'Failed to delete user'); + throw new InternalServerErrorException('Failed to delete user'); + } + } + + async restoreUser(authUser: AuthUserDto, userId: string): Promise { + const requestor = await this.userRepository.get(authUser.id); + if (!requestor) { + throw new UnauthorizedException('Requestor not found'); + } + if (!requestor.isAdmin) { + throw new ForbiddenException('Unauthorized'); + } + const user = await this.userRepository.get(userId, true); + if (!user) { + throw new BadRequestException('User not found'); + } + try { + const restoredUser = await this.userRepository.restore(user); + return mapUser(restoredUser); + } catch (e) { + Logger.error(e, 'Failed to restore deleted user'); + throw new InternalServerErrorException('Failed to restore deleted user'); + } + } + async createProfileImage( authUser: AuthUserDto, fileInfo: Express.Multer.File, diff --git a/server/apps/immich/src/modules/background-task/background-task.processor.ts b/server/apps/immich/src/modules/background-task/background-task.processor.ts index d852f734c2..01cdfb7864 100644 --- a/server/apps/immich/src/modules/background-task/background-task.processor.ts +++ b/server/apps/immich/src/modules/background-task/background-task.processor.ts @@ -2,10 +2,10 @@ import { Process, Processor } from '@nestjs/bull'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AssetEntity } from '@app/database/entities/asset.entity'; -import fs from 'fs'; import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { Job } from 'bull'; import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto'; +import { assetUtils } from '@app/common/utils'; @Processor('background-task') export class BackgroundTaskProcessor { @@ -23,37 +23,7 @@ export class BackgroundTaskProcessor { const { assets } = job.data; for (const asset of assets) { - fs.unlink(asset.originalPath, (err) => { - if (err) { - console.log('error deleting ', asset.originalPath); - } - }); - - // TODO: what if there is no asset.resizePath. Should fail the Job? - // => panoti report: Job not fail - if (asset.resizePath) { - fs.unlink(asset.resizePath, (err) => { - if (err) { - console.log('error deleting ', asset.resizePath); - } - }); - } - - if (asset.webpPath) { - fs.unlink(asset.webpPath, (err) => { - if (err) { - console.log('error deleting ', asset.webpPath); - } - }); - } - - if (asset.encodedVideoPath) { - fs.unlink(asset.encodedVideoPath, (err) => { - if (err) { - console.log('error deleting ', asset.encodedVideoPath); - } - }); - } + assetUtils.deleteFiles(asset); } } } diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts index e932a67f97..0e30e9ac66 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts @@ -5,10 +5,19 @@ import { AssetEntity } from '@app/database/entities/asset.entity'; import { ScheduleTasksService } from './schedule-tasks.service'; import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { ExifEntity } from '@app/database/entities/exif.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([AssetEntity, ExifEntity]), + TypeOrmModule.forFeature([AssetEntity, ExifEntity, UserEntity]), + BullModule.registerQueue({ + name: QueueNameEnum.USER_DELETION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), BullModule.registerQueue({ name: QueueNameEnum.VIDEO_CONVERSION, defaultJobOptions: { diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts index d814d8b307..80e5e9924d 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts @@ -8,6 +8,7 @@ import { Queue } from 'bull'; import { randomUUID } from 'crypto'; import { ExifEntity } from '@app/database/entities/exif.entity'; import { + userDeletionProcessorName, exifExtractionProcessorName, generateWEBPThumbnailProcessorName, IMetadataExtractionJob, @@ -18,10 +19,16 @@ import { videoMetadataExtractionProcessorName, } from '@app/job'; import { ConfigService } from '@nestjs/config'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface'; +import { userUtils } from '@app/common'; @Injectable() export class ScheduleTasksService { constructor( + @InjectRepository(UserEntity) + private userRepository: Repository, + @InjectRepository(AssetEntity) private assetRepository: Repository, @@ -37,6 +44,9 @@ export class ScheduleTasksService { @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) private metadataExtractionQueue: Queue, + @InjectQueue(QueueNameEnum.USER_DELETION) + private userDeletionQueue: Queue, + private configService: ConfigService, ) {} @@ -128,4 +138,14 @@ export class ScheduleTasksService { } } } + + @Cron(CronExpression.EVERY_DAY_AT_11PM) + async deleteUserAndRelatedAssets() { + const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } }); + for (const user of usersToDelete) { + if (userUtils.isReadyForDeletion(user)) { + await this.userDeletionQueue.add(userDeletionProcessorName, { user: user }, { jobId: randomUUID() }); + } + } + } } diff --git a/server/apps/immich/test/user.e2e-spec.ts b/server/apps/immich/test/user.e2e-spec.ts index 498927a453..4fe244b929 100644 --- a/server/apps/immich/test/user.e2e-spec.ts +++ b/server/apps/immich/test/user.e2e-spec.ts @@ -104,6 +104,7 @@ describe('User', () => { isAdmin: false, shouldChangePassword: true, profileImagePath: '', + deletedAt: null, }, { email: userTwoEmail, @@ -114,6 +115,7 @@ describe('User', () => { isAdmin: false, shouldChangePassword: true, profileImagePath: '', + deletedAt: null, }, ]), ); diff --git a/server/apps/microservices/src/processors/user-deletion.processor.ts b/server/apps/microservices/src/processors/user-deletion.processor.ts new file mode 100644 index 0000000000..fa2eb0264b --- /dev/null +++ b/server/apps/microservices/src/processors/user-deletion.processor.ts @@ -0,0 +1,35 @@ +import { APP_UPLOAD_LOCATION, userUtils } from '@app/common'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { QueueNameEnum, userDeletionProcessorName } from '@app/job'; +import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface'; +import { Process, Processor } from '@nestjs/bull'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Job } from 'bull'; +import { join } from 'path'; +import fs from 'fs'; +import { Repository } from 'typeorm'; + +@Processor(QueueNameEnum.USER_DELETION) +export class UserDeletionProcessor { + constructor( + @InjectRepository(UserEntity) + private userRepository: Repository, + + @InjectRepository(AssetEntity) + private assetRepository: Repository, + ) {} + + @Process(userDeletionProcessorName) + async processUserDeletion(job: Job) { + const { user } = job.data; + // just for extra protection here + if (userUtils.isReadyForDeletion(user)) { + const basePath = APP_UPLOAD_LOCATION; + const userAssetDir = join(basePath, user.id) + fs.rmSync(userAssetDir, { recursive: true, force: true }) + await this.assetRepository.delete({ userId: user.id }) + await this.userRepository.remove(user); + } + } +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 634421857a..dc862c5bb5 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"objects":{"type":"integer"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","objects","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"objects":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string","nullable":true}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin","deletedAt"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"objects":{"type":"integer"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","objects","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"objects":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file diff --git a/server/libs/common/src/utils/asset-utils.ts b/server/libs/common/src/utils/asset-utils.ts new file mode 100644 index 0000000000..7d590b269e --- /dev/null +++ b/server/libs/common/src/utils/asset-utils.ts @@ -0,0 +1,39 @@ +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { AssetResponseDto } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto'; +import fs from 'fs'; + +const deleteFiles = (asset: AssetEntity | AssetResponseDto) => { + fs.unlink(asset.originalPath, (err) => { + if (err) { + console.log('error deleting ', asset.originalPath); + } + }); + + // TODO: what if there is no asset.resizePath. Should fail the Job? + // => panoti report: Job not fail + if (asset.resizePath) { + fs.unlink(asset.resizePath, (err) => { + if (err) { + console.log('error deleting ', asset.resizePath); + } + }); + } + + if (asset.webpPath) { + fs.unlink(asset.webpPath, (err) => { + if (err) { + console.log('error deleting ', asset.webpPath); + } + }); + } + + if (asset.encodedVideoPath) { + fs.unlink(asset.encodedVideoPath, (err) => { + if (err) { + console.log('error deleting ', asset.encodedVideoPath); + } + }); + } +}; + +export const assetUtils = { deleteFiles }; diff --git a/server/libs/common/src/utils/index.ts b/server/libs/common/src/utils/index.ts index c276308273..afc90e5855 100644 --- a/server/libs/common/src/utils/index.ts +++ b/server/libs/common/src/utils/index.ts @@ -1 +1,3 @@ export * from './time-utils'; +export * from './asset-utils'; +export * from './user-utils'; diff --git a/server/libs/common/src/utils/user-utils.spec.ts b/server/libs/common/src/utils/user-utils.spec.ts new file mode 100644 index 0000000000..22252cf937 --- /dev/null +++ b/server/libs/common/src/utils/user-utils.spec.ts @@ -0,0 +1,19 @@ +// create unit test for user utils + +import { UserEntity } from '@app/database/entities/user.entity'; +import { userUtils } from './user-utils'; + +describe('User Utilities', () => { + describe('checkIsReadyForDeletion', () => { + it('check that user is not ready to be deleted', () => { + const result = userUtils.isReadyForDeletion({ deletedAt: new Date() } as UserEntity); + expect(result).toBeFalsy(); + }); + + it('check that user is ready to be deleted', () => { + const aWeekAgo = new Date(new Date().getTime() - 8 * 86400000); + const result = userUtils.isReadyForDeletion({ deletedAt: aWeekAgo } as UserEntity); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/server/libs/common/src/utils/user-utils.ts b/server/libs/common/src/utils/user-utils.ts new file mode 100644 index 0000000000..7a2e04cdc1 --- /dev/null +++ b/server/libs/common/src/utils/user-utils.ts @@ -0,0 +1,16 @@ +import { UserEntity } from '@app/database/entities/user.entity'; + +function createUserUtils() { + const isReadyForDeletion = (user: UserEntity): boolean => { + if (user.deletedAt == null) return false; + const millisecondsInDay = 86400000; + // get this number (7 days) from some configuration perhaps ? + const millisecondsDeleteWait = millisecondsInDay * 7; + + const millisecondsSinceDelete = new Date().getTime() - (user.deletedAt?.getTime() ?? 0); + return millisecondsSinceDelete >= millisecondsDeleteWait; + }; + return { isReadyForDeletion }; +} + +export const userUtils = createUserUtils(); diff --git a/server/libs/database/src/entities/user.entity.ts b/server/libs/database/src/entities/user.entity.ts index 4ba3e4afc7..649f226afe 100644 --- a/server/libs/database/src/entities/user.entity.ts +++ b/server/libs/database/src/entities/user.entity.ts @@ -1,4 +1,4 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'; @Entity('users') export class UserEntity { @@ -31,4 +31,7 @@ export class UserEntity { @CreateDateColumn() createdAt!: string; + + @DeleteDateColumn() + deletedAt?: Date; } diff --git a/server/libs/database/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts b/server/libs/database/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts new file mode 100644 index 0000000000..1e80fc089a --- /dev/null +++ b/server/libs/database/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddingDeletedAtColumnInUserEntity1667762360744 implements MigrationInterface { + name = 'AddingDeletedAtColumnInUserEntity1667762360744'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "deletedAt" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deletedAt"`); + } +} diff --git a/server/libs/job/src/constants/job-name.constant.ts b/server/libs/job/src/constants/job-name.constant.ts index 7b32c69028..9daea50082 100644 --- a/server/libs/job/src/constants/job-name.constant.ts +++ b/server/libs/job/src/constants/job-name.constant.ts @@ -29,3 +29,8 @@ export enum MachineLearningJobNameEnum { OBJECT_DETECTION = 'detect-object', IMAGE_TAGGING = 'tag-image', } + +/** + * User deletion Queue Jobs + */ +export const userDeletionProcessorName = 'user-deletion'; diff --git a/server/libs/job/src/constants/queue-name.constant.ts b/server/libs/job/src/constants/queue-name.constant.ts index f0f4c4e053..3ddf72ea79 100644 --- a/server/libs/job/src/constants/queue-name.constant.ts +++ b/server/libs/job/src/constants/queue-name.constant.ts @@ -5,4 +5,5 @@ export enum QueueNameEnum { CHECKSUM_GENERATION = 'generate-checksum-queue', ASSET_UPLOADED = 'asset-uploaded-queue', MACHINE_LEARNING = 'machine-learning-queue', + USER_DELETION = 'user-deletion-queue', } diff --git a/server/libs/job/src/interfaces/user-deletion.interface.ts b/server/libs/job/src/interfaces/user-deletion.interface.ts new file mode 100644 index 0000000000..88c18526a7 --- /dev/null +++ b/server/libs/job/src/interfaces/user-deletion.interface.ts @@ -0,0 +1,8 @@ +import { UserEntity } from '@app/database/entities/user.entity'; + +export interface IUserDeletionJob { + /** + * The user entity that was saved in the database + */ + user: UserEntity; +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 9360b2d7e4..2910b6433f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1575,6 +1575,12 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'isAdmin': boolean; + /** + * + * @type {string} + * @memberof UserResponseDto + */ + 'deletedAt': string | null; } /** * @@ -4711,6 +4717,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (userId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'userId' is not null or undefined + assertParamExists('deleteUser', 'userId', userId) + const localVarPath = `/user/{userId}` + .replace(`{${"userId"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {boolean} isAll @@ -4870,6 +4913,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreUser: async (userId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'userId' is not null or undefined + assertParamExists('restoreUser', 'userId', userId) + const localVarPath = `/user/{userId}/restore` + .replace(`{${"userId"}}`, encodeURIComponent(String(userId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -4948,6 +5028,16 @@ export const UserApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(userId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {boolean} isAll @@ -4996,6 +5086,16 @@ export const UserApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async restoreUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.restoreUser(userId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {UpdateUserDto} updateUserDto @@ -5034,6 +5134,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? createUser(createUserDto: CreateUserDto, options?: any): AxiosPromise { return localVarFp.createUser(createUserDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(userId: string, options?: any): AxiosPromise { + return localVarFp.deleteUser(userId, options).then((request) => request(axios, basePath)); + }, /** * * @param {boolean} isAll @@ -5077,6 +5186,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? getUserCount(options?: any): AxiosPromise { return localVarFp.getUserCount(options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreUser(userId: string, options?: any): AxiosPromise { + return localVarFp.restoreUser(userId, options).then((request) => request(axios, basePath)); + }, /** * * @param {UpdateUserDto} updateUserDto @@ -5118,6 +5236,17 @@ export class UserApi extends BaseAPI { return UserApiFp(this.configuration).createUser(createUserDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UserApi + */ + public deleteUser(userId: string, options?: AxiosRequestConfig) { + return UserApiFp(this.configuration).deleteUser(userId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {boolean} isAll @@ -5171,6 +5300,17 @@ export class UserApi extends BaseAPI { return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} userId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UserApi + */ + public restoreUser(userId: string, options?: AxiosRequestConfig) { + return UserApiFp(this.configuration).restoreUser(userId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {UpdateUserDto} updateUserDto diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte new file mode 100644 index 0000000000..8d9b4625cf --- /dev/null +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -0,0 +1,41 @@ + + +
+
+

+ Confirm User Deletion +

+
+
+

+ {user.firstName} + {user.lastName} account and assets along will be marked to delete completely after 7 days. are + you sure you want to proceed ? +

+ +
+ +
+
+
diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte new file mode 100644 index 0000000000..524ff9e47e --- /dev/null +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -0,0 +1,40 @@ + + +
+
+

+ Restore User +

+
+
+

+ {user.firstName} + {user.lastName} account will restored +

+ +
+ +
+
+
diff --git a/web/src/lib/components/admin-page/user-management.svelte b/web/src/lib/components/admin-page/user-management.svelte index b385af1d95..2ebe2c909f 100644 --- a/web/src/lib/components/admin-page/user-management.svelte +++ b/web/src/lib/components/admin-page/user-management.svelte @@ -3,9 +3,21 @@ import { createEventDispatcher } from 'svelte'; import PencilOutline from 'svelte-material-icons/PencilOutline.svelte'; + import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte'; + import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte'; + import moment from 'moment'; + export let allUsers: Array; const dispatch = createEventDispatcher(); + + const isDeleted = (user: UserResponseDto): boolean => { + return user.deletedAt != null; + }; + + const getDeleteDate = (user: UserResponseDto): string => { + return moment(user.deletedAt).add(7, 'days').format('LL'); + }; @@ -16,7 +28,7 @@ - + - + {/each} diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 9589d9b230..d56602cbfc 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -11,21 +11,25 @@ import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; + import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte'; import type { PageData } from './$types'; import { api, ServerStatsResponseDto, UserResponseDto } from '@api'; import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte'; + import RestoreDialoge from '$lib/components/admin-page/restore-dialoge.svelte'; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; export let data: PageData; - let editUser: UserResponseDto; + let selectedUser: UserResponseDto; let shouldShowEditUserForm = false; let shouldShowCreateUserForm = false; let shouldShowInfoPanel = false; + let shouldShowDeleteConfirmDialog = false; + let shouldShowRestoreDialog = false; let serverStat: ServerStatsResponseDto; const onButtonClicked = (buttonType: CustomEvent) => { @@ -45,7 +49,7 @@ const editUserHandler = async (event: CustomEvent) => { const { user } = event.detail; - editUser = user; + selectedUser = user; shouldShowEditUserForm = true; }; @@ -62,6 +66,43 @@ shouldShowInfoPanel = true; }; + const deleteUserHandler = async (event: CustomEvent) => { + const { user } = event.detail; + selectedUser = user; + shouldShowDeleteConfirmDialog = true; + }; + + const onUserDeleteSuccess = async () => { + const getAllUsersRes = await api.userApi.getAllUsers(false); + data.allUsers = getAllUsersRes.data; + shouldShowDeleteConfirmDialog = false; + }; + + const onUserDeleteFail = async () => { + const getAllUsersRes = await api.userApi.getAllUsers(false); + data.allUsers = getAllUsersRes.data; + shouldShowDeleteConfirmDialog = false; + }; + + const restoreUserHandler = async (event: CustomEvent) => { + const { user } = event.detail; + selectedUser = user; + shouldShowRestoreDialog = true; + }; + + const onUserRestoreSuccess = async () => { + const getAllUsersRes = await api.userApi.getAllUsers(false); + data.allUsers = getAllUsersRes.data; + shouldShowRestoreDialog = false; + }; + + const onUserRestoreFail = async () => { + // show fail dialog + const getAllUsersRes = await api.userApi.getAllUsers(false); + data.allUsers = getAllUsersRes.data; + shouldShowRestoreDialog = false; + }; + const getServerStats = async () => { try { const res = await api.serverInfoApi.getStats(); @@ -87,13 +128,33 @@ {#if shouldShowEditUserForm} (shouldShowEditUserForm = false)}> {/if} +{#if shouldShowDeleteConfirmDialog} + (shouldShowDeleteConfirmDialog = false)}> + + +{/if} + +{#if shouldShowRestoreDialog} + (shouldShowRestoreDialog = false)}> + + +{/if} + {#if shouldShowInfoPanel} (shouldShowInfoPanel = false)}>
@@ -160,6 +221,8 @@ allUsers={data.allUsers} on:create-user={() => (shouldShowCreateUserForm = true)} on:edit-user={editUserHandler} + on:delete-user={deleteUserHandler} + on:restore-user={restoreUserHandler} /> {/if} {#if selectedAction === AdminSideBarSelection.JOBS}
Email First name Last nameEditAction
{user.email} {user.firstName} {user.lastName} + {#if !isDeleted(user)} + + + {/if} + {#if isDeleted(user)} + + {/if} +