Parcourir la source

Refactor web to use OpenAPI SDK (#326)

* Refactor main index page

* Refactor admin page

* Refactor Auth endpoint

* Refactor directory to prep for monorepo

* Fixed refactoring path

* Resolved file path in vite

* Refactor photo index page

* Refactor thumbnail

* Fixed test

* Refactor Video Viewer component

* Refactor download file

* Refactor navigation bar

* Refactor upload file check

* Simplify Upload Asset signature

* PR feedback
Alex il y a 3 ans
Parent
commit
9a6dfacf9b
55 fichiers modifiés avec 520 ajouts et 695 suppressions
  1. 39 41
      server/apps/immich/src/api-v1/asset/asset.controller.ts
  2. 41 17
      server/apps/immich/src/api-v1/asset/asset.service.ts
  3. 10 0
      server/apps/immich/src/api-v1/asset/dto/asset-file-upload.dto.ts
  4. 24 5
      server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts
  5. 7 0
      server/apps/immich/src/api-v1/asset/response-dto/asset-file-upload-response.dto.ts
  6. 6 0
      server/apps/immich/src/api-v1/asset/response-dto/check-duplicate-asset-response.dto.ts
  7. 8 8
      server/apps/immich/src/api-v1/user/user.controller.ts
  8. 4 12
      server/apps/immich/src/api-v1/user/user.service.ts
  9. 1 0
      server/apps/immich/src/app.module.ts
  10. 1 1
      server/apps/immich/test/user.e2e-spec.ts
  11. 0 0
      server/immich-openapi-specs.json
  12. 1 1
      server/package-lock.json
  13. 1 1
      server/package.json
  14. 4 3
      web/src/api/api.ts
  15. 2 0
      web/src/api/index.ts
  16. 0 0
      web/src/api/open-api/.gitignore
  17. 0 0
      web/src/api/open-api/.npmignore
  18. 0 0
      web/src/api/open-api/.openapi-generator-ignore
  19. 0 0
      web/src/api/open-api/.openapi-generator/FILES
  20. 0 0
      web/src/api/open-api/.openapi-generator/VERSION
  21. 82 126
      web/src/api/open-api/api.ts
  22. 22 25
      web/src/api/open-api/base.ts
  23. 0 0
      web/src/api/open-api/common.ts
  24. 0 0
      web/src/api/open-api/configuration.ts
  25. 0 0
      web/src/api/open-api/git_push.sh
  26. 0 0
      web/src/api/open-api/index.ts
  27. 12 11
      web/src/hooks.ts
  28. 4 2
      web/src/lib/components/admin/user-management.svelte
  29. 27 23
      web/src/lib/components/asset-viewer/asset-viewer.svelte
  30. 4 5
      web/src/lib/components/asset-viewer/detail-panel.svelte
  31. 16 21
      web/src/lib/components/asset-viewer/immich-thumbnail.svelte
  32. 16 20
      web/src/lib/components/asset-viewer/photo-viewer.svelte
  33. 13 16
      web/src/lib/components/asset-viewer/video-viewer.svelte
  34. 1 3
      web/src/lib/components/forms/change-password-form.svelte
  35. 15 4
      web/src/lib/components/shared/navigation-bar.svelte
  36. 1 1
      web/src/lib/components/shared/status-box.svelte
  37. 4 5
      web/src/lib/stores/assets.ts
  38. 1 1
      web/src/lib/utils/api-helper.ts
  39. 8 11
      web/src/lib/utils/file-uploader.ts
  40. 20 13
      web/src/routes/__layout.svelte
  41. 29 39
      web/src/routes/admin/api/create-user.ts
  42. 9 11
      web/src/routes/admin/index.svelte
  43. 6 14
      web/src/routes/auth/change-password/index.svelte
  44. 15 16
      web/src/routes/auth/change-password/index.ts
  45. 0 11
      web/src/routes/auth/login/api/get-users.ts
  46. 0 52
      web/src/routes/auth/login/api/select-admin.ts
  47. 7 29
      web/src/routes/auth/login/index.ts
  48. 0 63
      web/src/routes/auth/login/update.ts
  49. 1 1
      web/src/routes/auth/logout.ts
  50. 5 7
      web/src/routes/auth/register/index.svelte
  51. 28 37
      web/src/routes/auth/register/index.ts
  52. 12 23
      web/src/routes/index.svelte
  53. 5 14
      web/src/routes/photos/index.svelte
  54. 2 1
      web/svelte.config.js
  55. 6 1
      web/tsconfig.json

+ 39 - 41
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -8,7 +8,6 @@ import {
   Get,
   Param,
   ValidationPipe,
-  StreamableFile,
   Query,
   Response,
   Headers,
@@ -16,13 +15,13 @@ import {
   Logger,
   HttpCode,
   BadRequestException,
+  UploadedFile,
 } from '@nestjs/common';
 import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
 import { AssetService } from './asset.service';
-import { FileFieldsInterceptor } from '@nestjs/platform-express';
+import { FileFieldsInterceptor, FileInterceptor } from '@nestjs/platform-express';
 import { assetUploadOption } from '../../config/asset-upload.config';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
-import { CreateAssetDto } from './dto/create-asset.dto';
 import { ServeFileDto } from './dto/serve-file.dto';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import { Response as Res } from 'express';
@@ -36,10 +35,14 @@ import { IAssetUploadedJob } from '@app/job/index';
 import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
 import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
-import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { ApiBearerAuth, ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { AssetResponseDto } from './response-dto/asset-response.dto';
+import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
+import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
+import { CreateAssetDto } from './dto/create-asset.dto';
+import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 
 @UseGuards(JwtAuthGuard)
 @ApiBearerAuth()
@@ -56,46 +59,43 @@ export class AssetController {
   ) {}
 
   @Post('upload')
-  @UseInterceptors(
-    FileFieldsInterceptor(
-      [
-        { name: 'assetData', maxCount: 1 },
-        { name: 'thumbnailData', maxCount: 1 },
-      ],
-      assetUploadOption,
-    ),
-  )
+  @UseInterceptors(FileInterceptor('assetData', assetUploadOption))
+  @ApiConsumes('multipart/form-data')
+  @ApiBody({
+    description: 'Asset Upload Information',
+    type: AssetFileUploadDto,
+  })
   async uploadFile(
     @GetAuthUser() authUser: AuthUserDto,
-    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[] },
+    @UploadedFile() file: Express.Multer.File,
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
-  ): Promise<'ok' | undefined> {
-    for (const file of uploadFiles.assetData) {
-      try {
-        const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
-
-        if (savedAsset) {
-          await this.assetUploadedQueue.add(
-            assetUploadedProcessorName,
-            { asset: savedAsset, fileName: file.originalname, fileSize: file.size },
-            { jobId: savedAsset.id },
-          );
-        }
-      } catch (e) {
-        Logger.error(`Error uploading file ${e}`);
-        throw new BadRequestException(`Error uploading file`, `${e}`);
+  ): Promise<AssetFileUploadResponseDto> {
+    try {
+      const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
+
+      if (!savedAsset) {
+        throw new BadRequestException('Asset not created');
       }
-    }
 
-    return 'ok';
+      await this.assetUploadedQueue.add(
+        assetUploadedProcessorName,
+        { asset: savedAsset, fileName: file.originalname, fileSize: file.size },
+        { jobId: savedAsset.id },
+      );
+
+      return new AssetFileUploadResponseDto(savedAsset.id);
+    } catch (e) {
+      Logger.error(`Error uploading file ${e}`);
+      throw new BadRequestException(`Error uploading file`, `${e}`);
+    }
   }
 
   @Get('/download')
   async downloadFile(
     @GetAuthUser() authUser: AuthUserDto,
     @Response({ passthrough: true }) res: Res,
-    @Query(ValidationPipe) query: ServeFileDto,
-  ): Promise<StreamableFile> {
+    @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
+  ): Promise<any> {
     return this.assetService.downloadFile(query, res);
   }
 
@@ -104,14 +104,14 @@ export class AssetController {
     @Headers() headers: Record<string, string>,
     @GetAuthUser() authUser: AuthUserDto,
     @Response({ passthrough: true }) res: Res,
-    @Query(ValidationPipe) query: ServeFileDto,
-  ): Promise<StreamableFile | undefined> {
+    @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
+  ): Promise<any> {
     return this.assetService.serveFile(authUser, query, res, headers);
   }
 
   @Get('/thumbnail/:assetId')
-  async getAssetThumbnail(@Param('assetId') assetId: string) {
-    return await this.assetService.getAssetThumbnail(assetId);
+  async getAssetThumbnail(@Param('assetId') assetId: string): Promise<any> {
+    return this.assetService.getAssetThumbnail(assetId);
   }
 
   @Get('/allObjects')
@@ -195,11 +195,9 @@ export class AssetController {
   async checkDuplicateAsset(
     @GetAuthUser() authUser: AuthUserDto,
     @Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto,
-  ) {
+  ): Promise<CheckDuplicateAssetResponseDto> {
     const res = await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
 
-    return {
-      isExist: res,
-    };
+    return new CheckDuplicateAssetResponseDto(res);
   }
 }

+ 41 - 17
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -9,7 +9,6 @@ import {
 import { InjectRepository } from '@nestjs/typeorm';
 import { IsNull, Not, Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
-import { CreateAssetDto } from './dto/create-asset.dto';
 import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
 import { constants, createReadStream, ReadStream, stat } from 'fs';
 import { ServeFileDto } from './dto/serve-file.dto';
@@ -21,6 +20,8 @@ import fs from 'fs/promises';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
+import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
+import { CreateAssetDto } from './dto/create-asset.dto';
 
 const fileInfo = promisify(stat);
 
@@ -132,8 +133,10 @@ export class AssetService {
       let fileReadStream = null;
       const asset = await this.findAssetOfDevice(query.did, query.aid);
 
-      if (query.isThumb === 'false' || !query.isThumb) {
+      // Download Video
+      if (asset.type === AssetType.VIDEO) {
         const { size } = await fileInfo(asset.originalPath);
+
         res.set({
           'Content-Type': asset.mimeType,
           'Content-Length': size,
@@ -142,22 +145,43 @@ export class AssetService {
         await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
         fileReadStream = createReadStream(asset.originalPath);
       } else {
-        if (!asset.resizePath) {
-          throw new NotFoundException('resizePath not set');
-        }
-        const { size } = await fileInfo(asset.resizePath);
-        res.set({
-          'Content-Type': 'image/jpeg',
-          'Content-Length': size,
-        });
+        // Download Image
+        if (!query.isThumb) {
+          /**
+           * Download Image Original File
+           */
+          const { size } = await fileInfo(asset.originalPath);
 
-        await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
-        fileReadStream = createReadStream(asset.resizePath);
+          res.set({
+            'Content-Type': asset.mimeType,
+            'Content-Length': size,
+          });
+
+          await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
+          fileReadStream = createReadStream(asset.originalPath);
+        } else {
+          /**
+           * Download Image Resize File
+           */
+          if (!asset.resizePath) {
+            throw new NotFoundException('resizePath not set');
+          }
+
+          const { size } = await fileInfo(asset.resizePath);
+
+          res.set({
+            'Content-Type': 'image/jpeg',
+            'Content-Length': size,
+          });
+
+          await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
+          fileReadStream = createReadStream(asset.resizePath);
+        }
       }
 
       return new StreamableFile(fileReadStream);
     } catch (e) {
-      Logger.error(`Error download asset`, 'downloadFile');
+      Logger.error(`Error download asset ${e}`, 'downloadFile');
       throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
     }
   }
@@ -177,7 +201,7 @@ export class AssetService {
         fileReadStream = createReadStream(asset.webpPath);
       } else {
         if (!asset.resizePath) {
-          return new NotFoundException('resizePath not set');
+          throw new NotFoundException('resizePath not set');
         }
 
         await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
@@ -203,7 +227,7 @@ export class AssetService {
     }
 
     // Handle Sending Images
-    if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
+    if (asset.type == AssetType.IMAGE) {
       try {
         /**
          * Serve file viewer on the web
@@ -225,7 +249,7 @@ export class AssetService {
         /**
          * Serve thumbnail image for both web and mobile app
          */
-        if (query.isThumb === 'false' || !query.isThumb) {
+        if (!query.isThumb) {
           res.set({
             'Content-Type': asset.mimeType,
           });
@@ -262,7 +286,7 @@ export class AssetService {
           `Cannot read thumbnail file for asset ${asset.id} - contact your administrator`,
         );
       }
-    } else if (asset.type == AssetType.VIDEO) {
+    } else {
       try {
         // Handle Video
         let videoPath = asset.originalPath;

+ 10 - 0
server/apps/immich/src/api-v1/asset/dto/asset-file-upload.dto.ts

@@ -0,0 +1,10 @@
+import { AssetType } from '@app/database/entities/asset.entity';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
+import { CreateAssetDto } from './create-asset.dto';
+
+export class AssetFileUploadDto {
+  @IsNotEmpty()
+  @ApiProperty({ type: 'string', format: 'binary' })
+  assetData!: any;
+}

+ 24 - 5
server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts

@@ -1,5 +1,6 @@
 import { ApiProperty } from '@nestjs/swagger';
-import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
+import { Transform, Type } from 'class-transformer';
+import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
 
 export class ServeFileDto {
   @IsNotEmpty()
@@ -11,10 +12,28 @@ export class ServeFileDto {
   did!: string;
 
   @IsOptional()
-  @IsBooleanString()
-  isThumb?: string;
+  @IsBoolean()
+  @Transform(({ value }) => {
+    if (value == 'true') {
+      return true;
+    } else if (value == 'false') {
+      return false;
+    }
+    return value;
+  })
+  @ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' })
+  isThumb?: boolean;
 
   @IsOptional()
-  @IsBooleanString()
-  isWeb?: string;
+  @IsBoolean()
+  @Transform(({ value }) => {
+    if (value == 'true') {
+      return true;
+    } else if (value == 'false') {
+      return false;
+    }
+    return value;
+  })
+  @ApiProperty({ type: Boolean, title: 'Is request made from web' })
+  isWeb?: boolean;
 }

+ 7 - 0
server/apps/immich/src/api-v1/asset/response-dto/asset-file-upload-response.dto.ts

@@ -0,0 +1,7 @@
+export class AssetFileUploadResponseDto {
+  constructor(id: string) {
+    this.id = id;
+  }
+
+  id: string;
+}

+ 6 - 0
server/apps/immich/src/api-v1/asset/response-dto/check-duplicate-asset-response.dto.ts

@@ -0,0 +1,6 @@
+export class CheckDuplicateAssetResponseDto {
+  constructor(isExist: boolean) {
+    this.isExist = isExist;
+  }
+  isExist: boolean;
+}

+ 8 - 8
server/apps/immich/src/api-v1/user/user.controller.ts

@@ -12,6 +12,7 @@ import {
   UploadedFile,
   Response,
   StreamableFile,
+  ParseBoolPipe,
 } from '@nestjs/common';
 import { UserService } from './user.service';
 import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
@@ -24,7 +25,6 @@ import { profileImageUploadOption } from '../../config/profile-image-upload.conf
 import { Response as Res } from 'express';
 import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
 import { UserResponseDto } from './response-dto/user-response.dto';
-import { UserEntity } from '@app/database/entities/user.entity';
 import { UserCountResponseDto } from './response-dto/user-count-response.dto';
 import { CreateProfileImageDto } from './dto/create-profile-image.dto';
 import { CreateProfileImageResponseDto } from './response-dto/create-profile-image-response.dto';
@@ -37,7 +37,10 @@ export class UserController {
   @UseGuards(JwtAuthGuard)
   @ApiBearerAuth()
   @Get()
-  async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
+  async getAllUsers(
+    @GetAuthUser() authUser: AuthUserDto,
+    @Query('isAll', ParseBoolPipe) isAll: boolean,
+  ): Promise<UserResponseDto[]> {
     return await this.userService.getAllUsers(authUser, isAll);
   }
 
@@ -57,8 +60,8 @@ export class UserController {
   }
 
   @Get('/count')
-  async getUserCount(@Query('isAdmin') isAdmin: boolean): Promise<UserCountResponseDto> {
-    return await this.userService.getUserCount(isAdmin);
+  async getUserCount(): Promise<UserCountResponseDto> {
+    return await this.userService.getUserCount();
   }
 
   @UseGuards(JwtAuthGuard)
@@ -84,10 +87,7 @@ export class UserController {
   }
 
   @Get('/profile-image/:userId')
-  async getProfileImage(
-    @Param('userId') userId: string,
-    @Response({ passthrough: true }) res: Res,
-  ): Promise<StreamableFile | undefined> {
+  async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res): Promise<any> {
     return this.userService.getUserProfileImage(userId, res);
   }
 }

+ 4 - 12
server/apps/immich/src/api-v1/user/user.service.ts

@@ -32,7 +32,6 @@ export class UserService {
   async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
     if (isAll) {
       const allUsers = await this.userRepository.find();
-
       return allUsers.map(mapUser);
     }
 
@@ -54,14 +53,8 @@ export class UserService {
     return mapUser(user);
   }
 
-  async getUserCount(isAdmin: boolean): Promise<UserCountResponseDto> {
-    let users;
-
-    if (isAdmin) {
-      users = await this.userRepository.find({ where: { isAdmin: true } });
-    } else {
-      users = await this.userRepository.find();
-    }
+  async getUserCount(): Promise<UserCountResponseDto> {
+    const users = await this.userRepository.find();
 
     return mapUserCountResponse(users.length);
   }
@@ -157,8 +150,7 @@ export class UserService {
       }
 
       if (!user.profileImagePath) {
-        res.status(404).send('User does not have a profile image');
-        return;
+        throw new NotFoundException('User does not have a profile image');
       }
 
       res.set({
@@ -167,7 +159,7 @@ export class UserService {
       const fileStream = createReadStream(user.profileImagePath);
       return new StreamableFile(fileStream);
     } catch (e) {
-      res.status(404).send('User does not have a profile image');
+      throw new NotFoundException('User does not have a profile image');
     }
   }
 }

+ 1 - 0
server/apps/immich/src/app.module.ts

@@ -15,6 +15,7 @@ import { AppController } from './app.controller';
 import { ScheduleModule } from '@nestjs/schedule';
 import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
 import { DatabaseModule } from '@app/database';
+import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
 
 @Module({
   imports: [

+ 1 - 1
server/apps/immich/test/user.e2e-spec.ts

@@ -86,7 +86,7 @@ describe('User', () => {
       });
 
       it('fetches the user collection excluding the auth user', async () => {
-        const { status, body } = await request(app.getHttpServer()).get('/user');
+        const { status, body } = await request(app.getHttpServer()).get('/user?isAll=false');
         expect(status).toEqual(200);
         expect(body).toHaveLength(2);
         expect(body).toEqual(

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
server/immich-openapi-specs.json


+ 1 - 1
server/package-lock.json

@@ -53,7 +53,7 @@
         "@nestjs/cli": "^8.2.8",
         "@nestjs/schematics": "^8.0.11",
         "@nestjs/testing": "^8.4.7",
-        "@openapitools/openapi-generator-cli": "^2.5.1",
+        "@openapitools/openapi-generator-cli": "2.5.1",
         "@types/bcrypt": "^5.0.0",
         "@types/bull": "^3.15.7",
         "@types/cron": "^2.0.0",

+ 1 - 1
server/package.json

@@ -23,7 +23,7 @@
     "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
     "test:e2e": "jest --config ./apps/immich/test/jest-e2e.json",
     "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
-    "api:generate-typescript": "rm -rf ../web/src/lib/open-api && npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/lib/open-api"
+    "api:generate-typescript": "rm -rf ../web/src/api/open-api && npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api"
   },
   "dependencies": {
     "@mapbox/mapbox-sdk": "^0.13.3",

+ 4 - 3
web/src/lib/immich-api/index.ts → web/src/api/api.ts

@@ -1,3 +1,4 @@
+import { serverEndpoint } from '$lib/constants';
 import {
 	AlbumApi,
 	AssetApi,
@@ -6,7 +7,7 @@ import {
 	DeviceInfoApi,
 	ServerInfoApi,
 	UserApi,
-} from '../open-api';
+} from './open-api';
 
 class ImmichApi {
 	public userApi: UserApi;
@@ -15,7 +16,7 @@ class ImmichApi {
 	public authenticationApi: AuthenticationApi;
 	public deviceInfoApi: DeviceInfoApi;
 	public serverInfoApi: ServerInfoApi;
-	private config = new Configuration();
+	private config = new Configuration({ basePath: serverEndpoint });
 
 	constructor() {
 		this.userApi = new UserApi(this.config);
@@ -31,4 +32,4 @@ class ImmichApi {
 	}
 }
 
-export const immichApi = new ImmichApi();
+export const api = new ImmichApi();

+ 2 - 0
web/src/api/index.ts

@@ -0,0 +1,2 @@
+export * from './open-api';
+export * from './api';

+ 0 - 0
web/src/lib/open-api/.gitignore → web/src/api/open-api/.gitignore


+ 0 - 0
web/src/lib/open-api/.npmignore → web/src/api/open-api/.npmignore


+ 0 - 0
web/src/lib/open-api/.openapi-generator-ignore → web/src/api/open-api/.openapi-generator-ignore


+ 0 - 0
web/src/lib/open-api/.openapi-generator/FILES → web/src/api/open-api/.openapi-generator/FILES


+ 0 - 0
web/src/lib/open-api/.openapi-generator/VERSION → web/src/api/open-api/.openapi-generator/VERSION


+ 82 - 126
web/src/lib/open-api/api.ts → web/src/api/open-api/api.ts

@@ -139,6 +139,19 @@ export interface AlbumResponseDto {
      */
     'assets': Array<AssetResponseDto>;
 }
+/**
+ * 
+ * @export
+ * @interface AssetFileUploadResponseDto
+ */
+export interface AssetFileUploadResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetFileUploadResponseDto
+     */
+    'id': string;
+}
 /**
  * 
  * @export
@@ -271,6 +284,19 @@ export interface CheckDuplicateAssetDto {
      */
     'deviceId': string;
 }
+/**
+ * 
+ * @export
+ * @interface CheckDuplicateAssetResponseDto
+ */
+export interface CheckDuplicateAssetResponseDto {
+    /**
+     * 
+     * @type {boolean}
+     * @memberof CheckDuplicateAssetResponseDto
+     */
+    'isExist': boolean;
+}
 /**
  * 
  * @export
@@ -296,71 +322,6 @@ export interface CreateAlbumDto {
      */
     'assetIds'?: Array<string>;
 }
-/**
- * 
- * @export
- * @interface CreateAssetDto
- */
-export interface CreateAssetDto {
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'deviceAssetId': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'deviceId': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'assetType': CreateAssetDtoAssetTypeEnum;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'createdAt': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'modifiedAt': string;
-    /**
-     * 
-     * @type {boolean}
-     * @memberof CreateAssetDto
-     */
-    'isFavorite': boolean;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'fileExtension': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof CreateAssetDto
-     */
-    'duration'?: string;
-}
-
-export const CreateAssetDtoAssetTypeEnum = {
-    Image: 'IMAGE',
-    Video: 'VIDEO',
-    Audio: 'AUDIO',
-    Other: 'OTHER'
-} as const;
-
-export type CreateAssetDtoAssetTypeEnum = typeof CreateAssetDtoAssetTypeEnum[keyof typeof CreateAssetDtoAssetTypeEnum];
-
 /**
  * 
  * @export
@@ -1879,12 +1840,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadFile: async (aid: string, did: string, isThumb?: string, isWeb?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        downloadFile: async (aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'aid' is not null or undefined
             assertParamExists('downloadFile', 'aid', aid)
             // verify required parameter 'did' is not null or undefined
@@ -2221,12 +2182,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        serveFile: async (aid: string, did: string, isThumb?: string, isWeb?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        serveFile: async (aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'aid' is not null or undefined
             assertParamExists('serveFile', 'aid', aid)
             // verify required parameter 'did' is not null or undefined
@@ -2276,13 +2237,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {CreateAssetDto} createAssetDto 
+         * @param {any} assetData 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile: async (createAssetDto: CreateAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'createAssetDto' is not null or undefined
-            assertParamExists('uploadFile', 'createAssetDto', createAssetDto)
+        uploadFile: async (assetData: any, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'assetData' is not null or undefined
+            assertParamExists('uploadFile', 'assetData', assetData)
             const localVarPath = `/asset/upload`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -2294,19 +2255,24 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
+            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();
 
             // authentication bearer required
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
 
+            if (assetData !== undefined) { 
+                localVarFormParams.append('assetData', assetData as any);
+            }
+    
+    
+            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
     
-            localVarHeaderParameter['Content-Type'] = 'application/json';
-
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(createAssetDto, localVarRequestOptions, configuration)
+            localVarRequestOptions.data = localVarFormParams;
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -2330,7 +2296,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+        async checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CheckDuplicateAssetResponseDto>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.checkDuplicateAsset(checkDuplicateAssetDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
@@ -2348,12 +2314,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async downloadFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+        async downloadFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(aid, did, isThumb, isWeb, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
@@ -2440,23 +2406,23 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async serveFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+        async serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.serveFile(aid, did, isThumb, isWeb, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
          * 
-         * @param {CreateAssetDto} createAssetDto 
+         * @param {any} assetData 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async uploadFile(createAssetDto: CreateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(createAssetDto, options);
+        async uploadFile(assetData: any, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetData, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
     }
@@ -2476,7 +2442,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: any): AxiosPromise<void> {
+        checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: any): AxiosPromise<CheckDuplicateAssetResponseDto> {
             return localVarFp.checkDuplicateAsset(checkDuplicateAssetDto, options).then((request) => request(axios, basePath));
         },
         /**
@@ -2492,12 +2458,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: any): AxiosPromise<void> {
+        downloadFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
             return localVarFp.downloadFile(aid, did, isThumb, isWeb, options).then((request) => request(axios, basePath));
         },
         /**
@@ -2575,22 +2541,22 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * 
          * @param {string} aid 
          * @param {string} did 
-         * @param {string} [isThumb] 
-         * @param {string} [isWeb] 
+         * @param {boolean} [isThumb] 
+         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        serveFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: any): AxiosPromise<void> {
+        serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
             return localVarFp.serveFile(aid, did, isThumb, isWeb, options).then((request) => request(axios, basePath));
         },
         /**
          * 
-         * @param {CreateAssetDto} createAssetDto 
+         * @param {any} assetData 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile(createAssetDto: CreateAssetDto, options?: any): AxiosPromise<string> {
-            return localVarFp.uploadFile(createAssetDto, options).then((request) => request(axios, basePath));
+        uploadFile(assetData: any, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
+            return localVarFp.uploadFile(assetData, options).then((request) => request(axios, basePath));
         },
     };
 };
@@ -2629,13 +2595,13 @@ export class AssetApi extends BaseAPI {
      * 
      * @param {string} aid 
      * @param {string} did 
-     * @param {string} [isThumb] 
-     * @param {string} [isWeb] 
+     * @param {boolean} [isThumb] 
+     * @param {boolean} [isWeb] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public downloadFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: AxiosRequestConfig) {
+    public downloadFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) {
         return AssetApiFp(this.configuration).downloadFile(aid, did, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
     }
 
@@ -2730,25 +2696,25 @@ export class AssetApi extends BaseAPI {
      * 
      * @param {string} aid 
      * @param {string} did 
-     * @param {string} [isThumb] 
-     * @param {string} [isWeb] 
+     * @param {boolean} [isThumb] 
+     * @param {boolean} [isWeb] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public serveFile(aid: string, did: string, isThumb?: string, isWeb?: string, options?: AxiosRequestConfig) {
+    public serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) {
         return AssetApiFp(this.configuration).serveFile(aid, did, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**
      * 
-     * @param {CreateAssetDto} createAssetDto 
+     * @param {any} assetData 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public uploadFile(createAssetDto: CreateAssetDto, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).uploadFile(createAssetDto, options).then((request) => request(this.axios, this.basePath));
+    public uploadFile(assetData: any, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).uploadFile(assetData, options).then((request) => request(this.axios, this.basePath));
     }
 }
 
@@ -3560,13 +3526,10 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
         },
         /**
          * 
-         * @param {boolean} isAdmin 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getUserCount: async (isAdmin: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'isAdmin' is not null or undefined
-            assertParamExists('getUserCount', 'isAdmin', isAdmin)
+        getUserCount: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/user/count`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -3579,10 +3542,6 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
-            if (isAdmin !== undefined) {
-                localVarQueryParameter['isAdmin'] = isAdmin;
-            }
-
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -3688,18 +3647,17 @@ export const UserApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async getProfileImage(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+        async getProfileImage(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getProfileImage(userId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
          * 
-         * @param {boolean} isAdmin 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async getUserCount(isAdmin: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserCountResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(isAdmin, options);
+        async getUserCount(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserCountResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -3763,17 +3721,16 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getProfileImage(userId: string, options?: any): AxiosPromise<void> {
+        getProfileImage(userId: string, options?: any): AxiosPromise<object> {
             return localVarFp.getProfileImage(userId, options).then((request) => request(axios, basePath));
         },
         /**
          * 
-         * @param {boolean} isAdmin 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getUserCount(isAdmin: boolean, options?: any): AxiosPromise<UserCountResponseDto> {
-            return localVarFp.getUserCount(isAdmin, options).then((request) => request(axios, basePath));
+        getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
+            return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -3850,13 +3807,12 @@ export class UserApi extends BaseAPI {
 
     /**
      * 
-     * @param {boolean} isAdmin 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof UserApi
      */
-    public getUserCount(isAdmin: boolean, options?: AxiosRequestConfig) {
-        return UserApiFp(this.configuration).getUserCount(isAdmin, options).then((request) => request(this.axios, this.basePath));
+    public getUserCount(options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
     }
 
     /**

+ 22 - 25
web/src/lib/open-api/base.ts → web/src/api/open-api/base.ts

@@ -5,29 +5,30 @@
  * Immich API
  *
  * The version of the OpenAPI document: 1.17.0
- *
+ * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * https://openapi-generator.tech
  * Do not edit the class manually.
  */
 
-import { Configuration } from './configuration';
+
+import { Configuration } from "./configuration";
 // Some imports not used depending on template conditions
 // @ts-ignore
 import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
 
-export const BASE_PATH = '/api'.replace(/\/+$/, '');
+export const BASE_PATH = "/api".replace(/\/+$/, "");
 
 /**
  *
  * @export
  */
 export const COLLECTION_FORMATS = {
-	csv: ',',
-	ssv: ' ',
-	tsv: '\t',
-	pipes: '|',
+    csv: ",",
+    ssv: " ",
+    tsv: "\t",
+    pipes: "|",
 };
 
 /**
@@ -36,8 +37,8 @@ export const COLLECTION_FORMATS = {
  * @interface RequestArgs
  */
 export interface RequestArgs {
-	url: string;
-	options: AxiosRequestConfig;
+    url: string;
+    options: AxiosRequestConfig;
 }
 
 /**
@@ -46,19 +47,15 @@ export interface RequestArgs {
  * @class BaseAPI
  */
 export class BaseAPI {
-	protected configuration: Configuration | undefined;
+    protected configuration: Configuration | undefined;
 
-	constructor(
-		configuration?: Configuration,
-		protected basePath: string = BASE_PATH,
-		protected axios: AxiosInstance = globalAxios,
-	) {
-		if (configuration) {
-			this.configuration = configuration;
-			this.basePath = configuration.basePath || this.basePath;
-		}
-	}
-}
+    constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
+        if (configuration) {
+            this.configuration = configuration;
+            this.basePath = configuration.basePath || this.basePath;
+        }
+    }
+};
 
 /**
  *
@@ -67,8 +64,8 @@ export class BaseAPI {
  * @extends {Error}
  */
 export class RequiredError extends Error {
-	name: 'RequiredError' = 'RequiredError';
-	constructor(public field: string, msg?: string) {
-		super(msg);
-	}
+    name: "RequiredError" = "RequiredError";
+    constructor(public field: string, msg?: string) {
+        super(msg);
+    }
 }

+ 0 - 0
web/src/lib/open-api/common.ts → web/src/api/open-api/common.ts


+ 0 - 0
web/src/lib/open-api/configuration.ts → web/src/api/open-api/configuration.ts


+ 0 - 0
web/src/lib/open-api/git_push.sh → web/src/api/open-api/git_push.sh


+ 0 - 0
web/src/lib/open-api/index.ts → web/src/api/open-api/index.ts


+ 12 - 11
web/src/hooks.ts

@@ -1,7 +1,7 @@
-import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit';
+import type { GetSession, Handle } from '@sveltejs/kit';
 import * as cookie from 'cookie';
-import { serverEndpoint } from '$lib/constants';
-import { session } from '$app/stores';
+import { api } from '@api';
+import { AxiosError } from 'axios';
 
 export const handle: Handle = async ({ event, resolve }) => {
 	const cookies = cookie.parse(event.request.headers.get('cookie') || '');
@@ -13,14 +13,10 @@ export const handle: Handle = async ({ event, resolve }) => {
 	try {
 		const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session);
 
-		const res = await fetch(`${serverEndpoint}/auth/validateToken`, {
-			method: 'POST',
-			headers: {
-				Authorization: `Bearer ${accessToken}`,
-			},
-		});
+		api.setAccessToken(accessToken);
+		const { status } = await api.authenticationApi.validateAccessToken();
 
-		if (res.status === 201) {
+		if (status === 201) {
 			event.locals.user = {
 				id,
 				accessToken,
@@ -35,7 +31,12 @@ export const handle: Handle = async ({ event, resolve }) => {
 
 		return response;
 	} catch (error) {
-		console.log('Error parsing session', error);
+		if (error instanceof AxiosError) {
+			console.log('Error validating token');
+			return await resolve(event);
+		}
+
+		console.log('Error parsing session');
 		return await resolve(event);
 	}
 };

+ 4 - 2
web/src/lib/components/admin/user-management.svelte

@@ -1,7 +1,9 @@
 <script lang="ts">
+	import { UserResponseDto } from '@api';
+
 	import { createEventDispatcher } from 'svelte';
 	import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
-	export let usersOnServer: Array<any>;
+	export let allUsers: Array<UserResponseDto>;
 
 	const dispatch = createEventDispatcher();
 </script>
@@ -18,7 +20,7 @@
 		</tr>
 	</thead>
 	<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border">
-		{#each usersOnServer as user, i}
+		{#each allUsers as user, i}
 			<tr
 				class={`text-center flex place-items-center w-full border-b h-[80px] ${
 					i % 2 == 0 ? 'bg-gray-100' : 'bg-immich-bg'

+ 27 - 23
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -1,22 +1,21 @@
 <script lang="ts">
 	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
-	import { fly, slide } from 'svelte/transition';
+	import { fly } from 'svelte/transition';
 	import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
 	import { flattenAssetGroupByDate } from '$lib/stores/assets';
 	import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
 	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
-	import { AssetType, type ImmichAsset, type ImmichExif } from '../../models/immich-asset';
+	import { AssetType } from '../../models/immich-asset';
 	import PhotoViewer from './photo-viewer.svelte';
 	import DetailPanel from './detail-panel.svelte';
 	import { session } from '$app/stores';
-	import { serverEndpoint } from '../../constants';
-	import axios from 'axios';
 	import { downloadAssets } from '$lib/stores/download';
 	import VideoViewer from './video-viewer.svelte';
+	import { api, AssetResponseDto } from '@api';
 
 	const dispatch = createEventDispatcher();
 
-	export let selectedAsset: ImmichAsset;
+	export let selectedAsset: AssetResponseDto;
 
 	export let selectedIndex: number;
 
@@ -99,8 +98,6 @@
 
 	const downloadFile = async () => {
 		if ($session.user) {
-			const url = `${serverEndpoint}/asset/download?aid=${selectedAsset.deviceAssetId}&did=${selectedAsset.deviceId}&isThumb=false`;
-
 			try {
 				const imageName = selectedAsset.exifInfo?.imageName ? selectedAsset.exifInfo?.imageName : selectedAsset.id;
 				const imageExtension = selectedAsset.originalPath.split('.')[1];
@@ -112,24 +109,31 @@
 				}
 				$downloadAssets[imageFileName] = 0;
 
-				const res = await axios.get(url, {
-					responseType: 'blob',
-					headers: {
-						Authorization: 'Bearer ' + $session.user.accessToken,
-					},
-					onDownloadProgress: (progressEvent) => {
-						if (progressEvent.lengthComputable) {
-							const total = progressEvent.total;
-							const current = progressEvent.loaded;
-							let percentCompleted = Math.floor((current / total) * 100);
-
-							$downloadAssets[imageFileName] = percentCompleted;
-						}
+				const { data, status } = await api.assetApi.downloadFile(
+					selectedAsset.deviceAssetId,
+					selectedAsset.deviceId,
+					false,
+					false,
+					{
+						responseType: 'blob',
+						onDownloadProgress: (progressEvent) => {
+							if (progressEvent.lengthComputable) {
+								const total = progressEvent.total;
+								const current = progressEvent.loaded;
+								let percentCompleted = Math.floor((current / total) * 100);
+
+								$downloadAssets[imageFileName] = percentCompleted;
+							}
+						},
 					},
-				});
+				);
+
+				if (!(data instanceof Blob)) {
+					return;
+				}
 
-				if (res.status === 200) {
-					const fileUrl = URL.createObjectURL(new Blob([res.data]));
+				if (status === 200) {
+					const fileUrl = URL.createObjectURL(data);
 					const anchor = document.createElement('a');
 					anchor.href = fileUrl;
 					anchor.download = imageFileName;

+ 4 - 5
web/src/lib/components/asset-viewer/detail-panel.svelte

@@ -5,24 +5,23 @@
 	import CameraIris from 'svelte-material-icons/CameraIris.svelte';
 	import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
 	import moment from 'moment';
-	import type { ImmichAsset } from '../../models/immich-asset';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import { browser } from '$app/env';
-	import { round } from 'lodash';
+	import { AssetResponseDto } from '@api';
 
 	// Map Property
 	let map: any;
 	let leaflet: any;
 	let marker: any;
 
-	export let asset: ImmichAsset;
-	$: if (asset.exifInfo) {
+	export let asset: AssetResponseDto;
+	$: if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
 		drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
 	}
 
 	onMount(async () => {
 		if (browser) {
-			if (asset.exifInfo) {
+			if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
 				await drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
 			}
 		}

+ 16 - 21
web/src/lib/components/asset-viewer/immich-thumbnail.svelte

@@ -1,18 +1,18 @@
 <script lang="ts">
-	import { AssetType, type ImmichAsset } from '../../models/immich-asset';
+	import { AssetType } from '../../models/immich-asset';
 	import { session } from '$app/stores';
 	import { createEventDispatcher, onDestroy } from 'svelte';
-	import { fade, fly, slide } from 'svelte/transition';
-	import { serverEndpoint } from '../../constants';
+	import { fade, fly } from 'svelte/transition';
 	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
 	import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
 	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
 	import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
 	import LoadingSpinner from '../shared/loading-spinner.svelte';
+	import { api, AssetResponseDto } from '@api';
 
 	const dispatch = createEventDispatcher();
 
-	export let asset: ImmichAsset;
+	export let asset: AssetResponseDto;
 	export let groupIndex: number;
 
 	let imageData: string;
@@ -29,33 +29,28 @@
 
 	const loadImageData = async () => {
 		if ($session.user) {
-			const res = await fetch(serverEndpoint + '/asset/thumbnail/' + asset.id, {
-				method: 'GET',
-				headers: {
-					Authorization: 'bearer ' + $session.user.accessToken,
-				},
-			});
-
-			imageData = URL.createObjectURL(await res.blob());
-
-			return imageData;
+			const { data } = await api.assetApi.getAssetThumbnail(asset.id, { responseType: 'blob' });
+			if (data instanceof Blob) {
+				imageData = URL.createObjectURL(data);
+				return imageData;
+			}
 		}
 	};
 
 	const loadVideoData = async () => {
 		isThumbnailVideoPlaying = false;
-		const videoUrl = `/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isWeb=true`;
 
 		if ($session.user) {
 			try {
-				const res = await fetch(serverEndpoint + videoUrl, {
-					method: 'GET',
-					headers: {
-						Authorization: 'bearer ' + $session.user.accessToken,
-					},
+				const { data } = await api.assetApi.serveFile(asset.deviceAssetId, asset.deviceId, false, true, {
+					responseType: 'blob',
 				});
 
-				videoData = URL.createObjectURL(await res.blob());
+				if (!(data instanceof Blob)) {
+					return;
+				}
+
+				videoData = URL.createObjectURL(data);
 
 				videoPlayerNode.src = videoData;
 				// videoPlayerNode.src = videoData + '#t=0,5';

+ 16 - 20
web/src/lib/components/asset-viewer/photo-viewer.svelte

@@ -1,43 +1,39 @@
 <script lang="ts">
 	import { session } from '$app/stores';
-	import { serverEndpoint } from '$lib/constants';
 	import { fade } from 'svelte/transition';
 
-	import type { ImmichAsset, ImmichExif } from '$lib/models/immich-asset';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import LoadingSpinner from '../shared/loading-spinner.svelte';
+	import { api, AssetResponseDto } from '@api';
 
 	export let assetId: string;
 	export let deviceId: string;
 
-	let assetInfo: ImmichAsset;
+	let assetInfo: AssetResponseDto;
 
 	const dispatch = createEventDispatcher();
 
 	onMount(async () => {
 		if ($session.user) {
-			const res = await fetch(serverEndpoint + '/asset/assetById/' + assetId, {
-				headers: {
-					Authorization: 'bearer ' + $session.user.accessToken,
-				},
-			});
-			assetInfo = await res.json();
+			const { data } = await api.assetApi.getAssetById(assetId);
+			assetInfo = data;
 		}
 	});
 
 	const loadAssetData = async () => {
-		const assetUrl = `/asset/file?aid=${assetInfo.deviceAssetId}&did=${deviceId}&isWeb=true`;
 		if ($session.user) {
-			const res = await fetch(serverEndpoint + assetUrl, {
-				method: 'GET',
-				headers: {
-					Authorization: 'bearer ' + $session.user.accessToken,
-				},
-			});
-
-			const assetData = URL.createObjectURL(await res.blob());
-
-			return assetData;
+			try {
+				const { data } = await api.assetApi.serveFile(assetInfo.deviceAssetId, deviceId, false, true, {
+					responseType: 'blob',
+				});
+
+				if (!(data instanceof Blob)) {
+					return;
+				}
+
+				const assetData = URL.createObjectURL(data);
+				return assetData;
+			} catch (e) {}
 		}
 	};
 </script>

+ 13 - 16
web/src/lib/components/asset-viewer/video-viewer.svelte

@@ -1,15 +1,14 @@
 <script lang="ts">
 	import { session } from '$app/stores';
-	import { serverEndpoint } from '$lib/constants';
 	import { fade } from 'svelte/transition';
 
-	import type { ImmichAsset, ImmichExif } from '$lib/models/immich-asset';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import LoadingSpinner from '../shared/loading-spinner.svelte';
+	import { api, AssetResponseDto } from '@api';
 
 	export let assetId: string;
 
-	let asset: ImmichAsset;
+	let asset: AssetResponseDto;
 
 	const dispatch = createEventDispatcher();
 
@@ -18,12 +17,9 @@
 
 	onMount(async () => {
 		if ($session.user) {
-			const res = await fetch(serverEndpoint + '/asset/assetById/' + assetId, {
-				headers: {
-					Authorization: 'bearer ' + $session.user.accessToken,
-				},
-			});
-			asset = await res.json();
+			const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
+
+			asset = assetInfo;
 
 			await loadVideoData();
 		}
@@ -31,17 +27,18 @@
 
 	const loadVideoData = async () => {
 		isVideoLoading = true;
-		const videoUrl = `/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isWeb=true`;
+
 		if ($session.user) {
 			try {
-				const res = await fetch(serverEndpoint + videoUrl, {
-					method: 'GET',
-					headers: {
-						Authorization: 'bearer ' + $session.user.accessToken,
-					},
+				const { data } = await api.assetApi.serveFile(asset.deviceAssetId, asset.deviceId, false, true, {
+					responseType: 'blob',
 				});
 
-				const videoData = URL.createObjectURL(await res.blob());
+				if (!(data instanceof Blob)) {
+					return;
+				}
+
+				const videoData = URL.createObjectURL(data);
 				videoPlayerNode.src = videoData;
 
 				videoPlayerNode.load();

+ 1 - 3
web/src/lib/components/forms/change-password-form.svelte

@@ -1,7 +1,5 @@
 <script lang="ts">
-	import { session } from '$app/stores';
-
-	import { sendRegistrationForm, sendUpdateForm } from '$lib/auth-api';
+	import { sendUpdateForm } from '$lib/auth-api';
 	import { createEventDispatcher } from 'svelte';
 	import type { ImmichUser } from '../../models/immich-user';
 

+ 15 - 4
web/src/lib/components/shared/navigation-bar.svelte

@@ -1,13 +1,14 @@
 <script lang="ts">
+	import { session } from '$app/stores';
 	import { goto } from '$app/navigation';
 	import { page } from '$app/stores';
 	import type { ImmichUser } from '$lib/models/immich-user';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import { fade, fly, slide } from 'svelte/transition';
-	import { postRequest } from '../../api';
 	import { serverEndpoint } from '../../constants';
 	import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
 	import { clickOutside } from './click-outside';
+	import { api } from '@api';
 
 	export let user: ImmichUser;
 
@@ -16,12 +17,22 @@
 
 	const dispatch = createEventDispatcher();
 	let shouldShowAccountInfoPanel = false;
-	onMount(async () => {
-		const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
 
-		if (res.status == 200) shouldShowProfileImage = true;
+	onMount(() => {
+		getUserProfileImage();
 	});
 
+	const getUserProfileImage = async () => {
+		if ($session.user) {
+			try {
+				await api.userApi.getProfileImage(user.id);
+				shouldShowProfileImage = true;
+			} catch (e) {
+				console.log('User does not have a profile image');
+				shouldShowProfileImage = false;
+			}
+		}
+	};
 	const getFirstLetter = (text?: string) => {
 		return text?.charAt(0).toUpperCase();
 	};

+ 1 - 1
web/src/lib/components/shared/status-box.svelte

@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { getRequest } from '$lib/api';
+	import { getRequest } from '$lib/utils/api-helper';
 	import { onDestroy, onMount } from 'svelte';
 	import { serverEndpoint } from '$lib/constants';
 	import Cloud from 'svelte-material-icons/Cloud.svelte';

+ 4 - 5
web/src/lib/stores/assets.ts

@@ -1,10 +1,9 @@
 import { writable, derived } from 'svelte/store';
-import { getRequest } from '$lib/api';
-import type { ImmichAsset } from '$lib/models/immich-asset';
 import lodash from 'lodash-es';
 import _ from 'lodash';
 import moment from 'moment';
-export const assets = writable<ImmichAsset[]>([]);
+import { api, AssetResponseDto } from '@api';
+export const assets = writable<AssetResponseDto[]>([]);
 
 export const assetsGroupByDate = derived(assets, ($assets) => {
 	try {
@@ -23,6 +22,6 @@ export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupB
 });
 
 export const getAssetsInfo = async (accessToken: string) => {
-	const res = await getRequest('asset', accessToken);
-	assets.set(res);
+	const { data } = await api.assetApi.getAllAssets();
+	assets.set(data);
 };

+ 1 - 1
web/src/lib/api.ts → web/src/lib/utils/api-helper.ts

@@ -1,4 +1,4 @@
-import { serverEndpoint } from './constants';
+import { serverEndpoint } from '../constants';
 
 type ISend = {
 	method: string;

+ 8 - 11
web/src/lib/utils/file-uploader.ts

@@ -1,7 +1,9 @@
+/* @vite-ignore */
 import * as exifr from 'exifr';
 import { serverEndpoint } from '../constants';
 import { uploadAssetsStore } from '$lib/stores/upload';
 import type { UploadAsset } from '../models/upload-asset';
+import { api } from '@api';
 
 export async function fileUploader(asset: File, accessToken: string) {
 	const assetType = asset.type.split('/')[0].toUpperCase();
@@ -51,19 +53,14 @@ export async function fileUploader(asset: File, accessToken: string) {
 		formData.append('assetData', asset);
 
 		// Check if asset upload on server before performing upload
-		const res = await fetch(serverEndpoint + '/asset/check', {
-			method: 'POST',
-			body: JSON.stringify({ deviceAssetId, deviceId: 'WEB' }),
-			headers: {
-				Authorization: 'Bearer ' + accessToken,
-				'Content-Type': 'application/json',
-			},
-		});
 
-		if (res.status === 200) {
-			const { isExist } = await res.json();
+		const { data, status } = await api.assetApi.checkDuplicateAsset({
+			deviceAssetId: String(deviceAssetId),
+			deviceId: 'WEB',
+		});
 
-			if (isExist) {
+		if (status === 200) {
+			if (data.isExist) {
 				return;
 			}
 		}

+ 20 - 13
web/src/routes/__layout.svelte

@@ -1,18 +1,15 @@
 <script context="module" lang="ts">
 	import type { Load } from '@sveltejs/kit';
 	import { checkAppVersion } from '$lib/utils/check-app-version';
-	import { browser } from '$app/env';
 
-	export const load: Load = async ({ url }) => {
-		if (browser) {
-			const { shouldShowAnnouncement, localVersion, remoteVersion } = await checkAppVersion();
-
-			return { props: { url, shouldShowAnnouncement, localVersion, remoteVersion } };
-		} else {
-			return {
-				props: { url },
-			};
+	export const load: Load = async ({ url, session }) => {
+		if (session.user) {
+			api.setAccessToken(session.user.accessToken);
 		}
+
+		return {
+			props: { url },
+		};
 	};
 </script>
 
@@ -24,11 +21,21 @@
 	import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
 	import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
 	import UploadPanel from '$lib/components/shared/upload-panel.svelte';
+	import { onMount } from 'svelte';
+	import { api } from '@api';
 
 	export let url: string;
-	export let shouldShowAnnouncement: boolean;
-	export let localVersion: string;
-	export let remoteVersion: string;
+	let shouldShowAnnouncement: boolean;
+	let localVersion: string;
+	let remoteVersion: string;
+
+	onMount(async () => {
+		const res = await checkAppVersion();
+
+		shouldShowAnnouncement = res.shouldShowAnnouncement;
+		localVersion = res.localVersion ?? 'unknown';
+		remoteVersion = res.remoteVersion ?? 'unknown';
+	});
 </script>
 
 <main>

+ 29 - 39
web/src/routes/admin/api/create-user.ts

@@ -1,44 +1,34 @@
 import type { RequestHandler } from '@sveltejs/kit';
-import { serverEndpoint } from '$lib/constants';
+import { api } from '@api';
 
-export const post: RequestHandler = async ({ request, locals }) => {
-  const form = await request.formData();
+export const post: RequestHandler = async ({ request }) => {
+	const form = await request.formData();
 
-  const email = form.get('email')
-  const password = form.get('password')
-  const firstName = form.get('firstName')
-  const lastName = form.get('lastName')
+	const email = form.get('email');
+	const password = form.get('password');
+	const firstName = form.get('firstName');
+	const lastName = form.get('lastName');
 
-  const payload = {
-    email,
-    password,
-    firstName,
-    lastName,
-  }
+	const { status } = await api.userApi.createUser({
+		email: String(email),
+		password: String(password),
+		firstName: String(firstName),
+		lastName: String(lastName),
+	});
 
-  const res = await fetch(`${serverEndpoint}/user`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-      'Authorization': `Bearer ${locals.user?.accessToken}`
-    },
-    body: JSON.stringify(payload),
-  })
-
-  if (res.status === 201) {
-    return {
-      status: 201,
-      body: {
-        success: 'Succesfully create user account'
-      }
-    }
-  } else {
-    return {
-      status: 400,
-      body: {
-        error: await res.json()
-      }
-    }
-
-  }
-}
+	if (status === 201) {
+		return {
+			status: 201,
+			body: {
+				success: 'Succesfully create user account',
+			},
+		};
+	} else {
+		return {
+			status: 400,
+			body: {
+				error: 'Error create user account',
+			},
+		};
+	}
+};

+ 9 - 11
web/src/routes/admin/index.svelte

@@ -1,8 +1,8 @@
 <script context="module" lang="ts">
 	import type { Load } from '@sveltejs/kit';
-	import { getRequest } from '$lib/api';
+	import { api, UserResponseDto } from '@api';
 
-	export const load: Load = async ({ session, fetch }) => {
+	export const load: Load = async ({ session }) => {
 		if (!session.user) {
 			return {
 				status: 302,
@@ -10,13 +10,13 @@
 			};
 		}
 
-		const usersOnServer = await getRequest('user', session.user.accessToken);
+		const { data } = await api.userApi.getAllUsers(false);
 
 		return {
 			status: 200,
 			props: {
 				user: session.user,
-				usersOnServer,
+				allUsers: data,
 			},
 		};
 	};
@@ -24,7 +24,6 @@
 
 <script lang="ts">
 	import { onMount } from 'svelte';
-	import { session } from '$app/stores';
 
 	import type { ImmichUser } from '$lib/models/immich-user';
 	import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
@@ -34,12 +33,12 @@
 	import UserManagement from '$lib/components/admin/user-management.svelte';
 	import FullScreenModal from '$lib/components/shared/full-screen-modal.svelte';
 	import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
-	import StatusBox from '../../lib/components/shared/status-box.svelte';
+	import StatusBox from '$lib/components/shared/status-box.svelte';
 
 	let selectedAction: AdminSideBarSelection;
 
 	export let user: ImmichUser;
-	export let usersOnServer: Array<ImmichUser>;
+	export let allUsers: UserResponseDto[];
 
 	let shouldShowCreateUserForm: boolean;
 
@@ -52,9 +51,8 @@
 	});
 
 	const onUserCreated = async () => {
-		if ($session.user) {
-			usersOnServer = await getRequest('user', $session.user.accessToken);
-		}
+		const { data } = await api.userApi.getAllUsers(false);
+		allUsers = data;
 
 		shouldShowCreateUserForm = false;
 	};
@@ -97,7 +95,7 @@
 		<section id="setting-content" class="relative pt-[85px] flex place-content-center">
 			<section class="w-[800px] pt-4">
 				{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
-					<UserManagement {usersOnServer} on:createUser={() => (shouldShowCreateUserForm = true)} />
+					<UserManagement {allUsers} on:createUser={() => (shouldShowCreateUserForm = true)} />
 				{/if}
 			</section>
 		</section>

+ 6 - 14
web/src/routes/auth/change-password/index.svelte

@@ -2,7 +2,6 @@
 	export const prerender = false;
 
 	import type { Load } from '@sveltejs/kit';
-	import type { ImmichUser } from '$lib/models/immich-user';
 
 	export const load: Load = async ({ session }) => {
 		if (!session.user) {
@@ -13,14 +12,7 @@
 		}
 
 		try {
-			const res = await fetch(serverEndpoint + '/user/me', {
-				method: 'GET',
-				headers: {
-					Authorization: 'Bearer ' + session.user.accessToken,
-				},
-			});
-
-			const userInfo: ImmichUser = await res.json();
+			const { data: userInfo } = await api.userApi.getMyUserInfo();
 
 			if (userInfo.shouldChangePassword) {
 				return {
@@ -47,15 +39,15 @@
 
 <script lang="ts">
 	import { goto } from '$app/navigation';
-	import { session } from '$app/stores';
-	import { onMount } from 'svelte';
 	import { fade } from 'svelte/transition';
-	import ChangePasswordForm from '../../../lib/components/forms/change-password-form.svelte';
-	import { serverEndpoint } from '../../../lib/constants';
 
-	export let user: ImmichUser;
+	import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
+	import { api, UserResponseDto } from '@api';
+
+	export let user: UserResponseDto;
 
 	const onSuccessHandler = async () => {
+		/** Svelte route fetch */
 		const res = await fetch('/auth/logout', { method: 'POST' });
 
 		if (res.status == 200 && res.statusText == 'OK') {

+ 15 - 16
web/src/routes/auth/change-password/index.ts

@@ -1,27 +1,26 @@
 import type { RequestHandler } from '@sveltejs/kit';
-import { serverEndpoint } from '$lib/constants';
+import { api } from '@api';
 
 export const post: RequestHandler = async ({ request, locals }) => {
-	const form = await request.formData();
+	if (!locals.user) {
+		return {
+			status: 401,
+			body: {
+				error: 'Unauthorized',
+			},
+		};
+	}
 
+	const form = await request.formData();
 	const password = form.get('password');
 
-	const payload = {
-		id: locals.user?.id,
-		password,
+	const { status } = await api.userApi.updateUser({
+		id: locals.user.id,
+		password: String(password),
 		shouldChangePassword: false,
-	};
-
-	const res = await fetch(`${serverEndpoint}/user`, {
-		method: 'PUT',
-		headers: {
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${locals.user?.accessToken}`,
-		},
-		body: JSON.stringify(payload),
 	});
 
-	if (res.status === 200) {
+	if (status === 200) {
 		return {
 			status: 200,
 			body: {
@@ -32,7 +31,7 @@ export const post: RequestHandler = async ({ request, locals }) => {
 		return {
 			status: 400,
 			body: {
-				error: await res.json(),
+				error: 'Error change password',
 			},
 		};
 	}

+ 0 - 11
web/src/routes/auth/login/api/get-users.ts

@@ -1,11 +0,0 @@
-import type { RequestHandler } from '@sveltejs/kit';
-import { getRequest } from '../../../../lib/api';
-
-export const get: RequestHandler = async ({ request, locals }) => {
-	const allUsers = await getRequest('user?isAll=true', locals.user!.accessToken);
-
-	return {
-		status: 200,
-		body: { allUsers },
-	};
-};

+ 0 - 52
web/src/routes/auth/login/api/select-admin.ts

@@ -1,52 +0,0 @@
-import type { RequestHandler } from '@sveltejs/kit';
-import { putRequest } from '$lib/api';
-import * as cookie from 'cookie';
-
-export const post: RequestHandler = async ({ request, locals }) => {
-
-  const { id, isAdmin } = await request.json()
-
-  const res = await putRequest('user', {
-    id,
-    isAdmin,
-  }, locals.user!.accessToken);
-
-
-
-  if (res.statusCode) {
-    return {
-      status: res.statusCode,
-      body: JSON.stringify(res)
-    }
-  }
-
-  if (res.id == locals.user!.id) {
-    return {
-      status: 200,
-      body: { userInfo: res },
-      headers: {
-        'Set-Cookie': cookie.serialize('session', JSON.stringify(
-          {
-            id: res.id,
-            accessToken: locals.user!.accessToken,
-            firstName: res.firstName,
-            lastName: res.lastName,
-            isAdmin: res.isAdmin,
-            email: res.email,
-          }), {
-          path: '/',
-          httpOnly: true,
-          sameSite: 'strict',
-          maxAge: 60 * 60 * 24 * 30,
-        })
-      }
-    }
-  } else {
-    return {
-      status: 200,
-      body: { userInfo: { ...locals.user! } },
-    }
-  }
-
-
-}

+ 7 - 29
web/src/routes/auth/login/index.ts

@@ -1,17 +1,6 @@
 import type { RequestHandler } from '@sveltejs/kit';
-import { serverEndpoint } from '$lib/constants';
 import * as cookie from 'cookie';
-import { getRequest, putRequest } from '$lib/api';
-
-type AuthUser = {
-	accessToken: string;
-	userId: string;
-	userEmail: string;
-	firstName: string;
-	lastName: string;
-	isAdmin: boolean;
-	shouldChangePassword: boolean;
-};
+import { api } from '@api';
 
 export const post: RequestHandler = async ({ request }) => {
 	const form = await request.formData();
@@ -19,22 +8,11 @@ export const post: RequestHandler = async ({ request }) => {
 	const email = form.get('email');
 	const password = form.get('password');
 
-	const payload = {
-		email,
-		password,
-	};
-
-	const res = await fetch(`${serverEndpoint}/auth/login`, {
-		method: 'POST',
-		headers: {
-			'Content-Type': 'application/json',
-		},
-		body: JSON.stringify(payload),
-	});
-
-	if (res.status === 201) {
-		// Login success
-		const authUser = (await res.json()) as AuthUser;
+	try {
+		const { data: authUser } = await api.authenticationApi.login({
+			email: String(email),
+			password: String(password),
+		});
 
 		return {
 			status: 200,
@@ -70,7 +48,7 @@ export const post: RequestHandler = async ({ request }) => {
 				),
 			},
 		};
-	} else {
+	} catch (error) {
 		return {
 			status: 400,
 			body: {

+ 0 - 63
web/src/routes/auth/login/update.ts

@@ -1,63 +0,0 @@
-import type { RequestHandler } from '@sveltejs/kit';
-import { putRequest } from '../../../lib/api';
-import * as cookie from 'cookie';
-
-export const post: RequestHandler = async ({ request, locals }) => {
-	const form = await request.formData();
-
-	const firstName = form.get('firstName');
-	const lastName = form.get('lastName');
-
-	if (locals.user) {
-		const updatedUser = await putRequest(
-			'user',
-			{
-				id: locals.user.id,
-				firstName,
-				lastName,
-			},
-			locals.user.accessToken,
-		);
-
-		return {
-			status: 200,
-			body: {
-				user: {
-					id: updatedUser.id,
-					accessToken: locals.user.accessToken,
-					firstName: updatedUser.firstName,
-					lastName: updatedUser.lastName,
-					isAdmin: updatedUser.isAdmin,
-					email: updatedUser.email,
-				},
-				success: 'Update user success',
-			},
-			headers: {
-				'Set-Cookie': cookie.serialize(
-					'session',
-					JSON.stringify({
-						id: updatedUser.id,
-						accessToken: locals.user.accessToken,
-						firstName: updatedUser.firstName,
-						lastName: updatedUser.lastName,
-						isAdmin: updatedUser.isAdmin,
-						email: updatedUser.email,
-					}),
-					{
-						path: '/',
-						httpOnly: true,
-						sameSite: 'strict',
-						maxAge: 60 * 60 * 24 * 30,
-					},
-				),
-			},
-		};
-	}
-
-	return {
-		status: 400,
-		body: {
-			error: 'Cannot get access token from cookies',
-		},
-	};
-};

+ 1 - 1
web/src/routes/auth/logout.ts

@@ -1,6 +1,6 @@
 import type { RequestHandler } from '@sveltejs/kit';
 
-export const post: RequestHandler = async ({ request }) => {
+export const post: RequestHandler = async () => {
 	return {
 		headers: {
 			'Set-Cookie': 'session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT',

+ 5 - 7
web/src/routes/auth/register/index.svelte

@@ -1,14 +1,11 @@
 <script context="module" lang="ts">
 	import type { Load } from '@sveltejs/kit';
-	import { serverEndpoint } from '$lib/constants';
 
-	export const load: Load = async ({ session, fetch }) => {
-		const res = await fetch(`${serverEndpoint}/user/count`);
-		const { userCount } = await res.json();
+	export const load: Load = async ({ session }) => {
+		const { data } = await api.userApi.getUserCount();
 
-		if (userCount != 0) {
+		if (data.userCount != 0) {
 			// Admin has been registered, redirect to login
-
 			if (!session.user) {
 				return {
 					status: 302,
@@ -17,7 +14,7 @@
 			} else {
 				return {
 					status: 302,
-					redirect: '/dashboard',
+					redirect: '/photos',
 				};
 			}
 		}
@@ -28,6 +25,7 @@
 
 <script lang="ts">
 	import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
+	import { api } from '@api';
 </script>
 
 <svelte:head>

+ 28 - 37
web/src/routes/auth/register/index.ts

@@ -1,43 +1,34 @@
 import type { RequestHandler } from '@sveltejs/kit';
-import { serverEndpoint } from '$lib/constants';
+import { api } from '@api';
 
 export const post: RequestHandler = async ({ request }) => {
-  const form = await request.formData();
+	const form = await request.formData();
 
-  const email = form.get('email')
-  const password = form.get('password')
-  const firstName = form.get('firstName')
-  const lastName = form.get('lastName')
+	const email = form.get('email');
+	const password = form.get('password');
+	const firstName = form.get('firstName');
+	const lastName = form.get('lastName');
 
-  const payload = {
-    email,
-    password,
-    firstName,
-    lastName,
-  }
+	const { status } = await api.authenticationApi.adminSignUp({
+		email: String(email),
+		password: String(password),
+		firstName: String(firstName),
+		lastName: String(lastName),
+	});
 
-  const res = await fetch(`${serverEndpoint}/auth/admin-sign-up`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json'
-    },
-    body: JSON.stringify(payload),
-  })
-
-  if (res.status === 201) {
-    return {
-      status: 201,
-      body: {
-        success: 'Succesfully create admin account'
-      }
-    }
-  } else {
-    return {
-      status: 400,
-      body: {
-        error: await res.json()
-      }
-    }
-
-  }
-}
+	if (status === 201) {
+		return {
+			status: 201,
+			body: {
+				success: 'Succesfully create admin account',
+			},
+		};
+	} else {
+		return {
+			status: 400,
+			body: {
+				error: 'Error create admin account',
+			},
+		};
+	}
+};

+ 12 - 23
web/src/routes/index.svelte

@@ -1,39 +1,28 @@
 <script context="module" lang="ts">
 	export const prerender = false;
 	import type { Load } from '@sveltejs/kit';
+	import { api } from '@api';
 
-	export const load: Load = async ({ session, fetch }) => {
-		const res = await fetch(`${serverEndpoint}/user/count`);
-		const { userCount } = await res.json();
-
-		if (!session.user) {
-			// Check if admin exist to wherether navigating to login or registration
-			if (userCount != 0) {
-				return {
-					status: 200,
-					props: {
-						isAdminUserExist: true,
-					},
-				};
-			} else {
-				return {
-					status: 200,
-					props: {
-						isAdminUserExist: false,
-					},
-				};
-			}
-		} else {
+	export const load: Load = async ({ session }) => {
+		const { data } = await api.userApi.getUserCount();
+
+		if (session.user) {
 			return {
 				status: 302,
 				redirect: '/photos',
 			};
 		}
+
+		return {
+			status: 200,
+			props: {
+				isAdminUserExist: data.userCount == 0 ? false : true,
+			},
+		};
 	};
 </script>
 
 <script lang="ts">
-	import { serverEndpoint } from '$lib/constants';
 	import { goto } from '$app/navigation';
 
 	export let isAdminUserExist: boolean;

+ 5 - 14
web/src/routes/photos/index.svelte

@@ -12,6 +12,8 @@
 			};
 		}
 
+		await getAssetsInfo(session.user.accessToken);
+
 		return {
 			status: 200,
 			props: {
@@ -30,17 +32,16 @@
 
 	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 	import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
-	import { onDestroy, onMount } from 'svelte';
+	import { onMount } from 'svelte';
 	import { fly } from 'svelte/transition';
 	import { session } from '$app/stores';
 	import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
 	import ImmichThumbnail from '$lib/components/asset-viewer/immich-thumbnail.svelte';
 	import moment from 'moment';
-	import type { ImmichAsset } from '$lib/models/immich-asset';
 	import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
 	import StatusBox from '$lib/components/shared/status-box.svelte';
 	import { fileUploader } from '$lib/utils/file-uploader';
-	import { openWebsocketConnection, closeWebsocketConnection } from '$lib/stores/websocket';
+	import { AssetResponseDto } from '@api';
 
 	export let user: ImmichUser;
 
@@ -54,7 +55,7 @@
 
 	let isShowAsset = false;
 	let currentViewAssetIndex = 0;
-	let currentSelectedAsset: ImmichAsset;
+	let currentSelectedAsset: AssetResponseDto;
 
 	const onButtonClicked = (buttonType: CustomEvent) => {
 		selectedAction = buttonType.detail['actionType'] as AppSideBarSelection;
@@ -62,16 +63,6 @@
 
 	onMount(async () => {
 		selectedAction = AppSideBarSelection.PHOTOS;
-
-		if ($session.user) {
-			await getAssetsInfo($session.user.accessToken);
-
-			openWebsocketConnection($session.user.accessToken);
-		}
-	});
-
-	onDestroy(() => {
-		closeWebsocketConnection();
 	});
 
 	const thumbnailMouseEventHandler = (event: CustomEvent) => {

+ 2 - 1
web/svelte.config.js

@@ -1,6 +1,6 @@
 import preprocess from 'svelte-preprocess';
 import adapter from '@sveltejs/adapter-node';
-
+import path from 'path';
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
 	preprocess: preprocess(),
@@ -14,6 +14,7 @@ const config = {
 			resolve: {
 				alias: {
 					'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
+					'@api': path.resolve('./src/api'),
 				},
 			},
 		},

+ 6 - 1
web/tsconfig.json

@@ -17,6 +17,11 @@
     "strict": true,
     "target": "es2020",
     "importsNotUsedAsValues": "preserve",
-    "preserveValueImports": false
+    "preserveValueImports": false,
+    "paths": {
+      "$lib": ["src/lib"],
+      "$lib/*": ["src/lib/*"],
+      "@api": ["src/api"]
+    }
   },
 }

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff