mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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); | ||||
|   ): Promise<AssetFileUploadResponseDto> { | ||||
|     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}`); | ||||
|       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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| export class AssetFileUploadResponseDto { | ||||
|   constructor(id: string) { | ||||
|     this.id = id; | ||||
|   } | ||||
|  | ||||
|   id: string; | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| export class CheckDuplicateAssetResponseDto { | ||||
|   constructor(isExist: boolean) { | ||||
|     this.isExist = isExist; | ||||
|   } | ||||
|   isExist: boolean; | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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: [ | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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
									
								
								web/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export * from './open-api'; | ||||
| export * from './api'; | ||||
| @@ -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)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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); | ||||
| 				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; | ||||
| 						} | ||||
| 								$downloadAssets[imageFileName] = percentCompleted; | ||||
| 							} | ||||
| 						}, | ||||
| 					}, | ||||
| 				}); | ||||
| 				); | ||||
|  | ||||
| 				if (res.status === 200) { | ||||
| 					const fileUrl = URL.createObjectURL(new Blob([res.data])); | ||||
| 				if (!(data instanceof Blob)) { | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				if (status === 200) { | ||||
| 					const fileUrl = URL.createObjectURL(data); | ||||
| 					const anchor = document.createElement('a'); | ||||
| 					anchor.href = fileUrl; | ||||
| 					anchor.download = imageFileName; | ||||
|   | ||||
| @@ -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); | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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, | ||||
| 				}, | ||||
| 			}); | ||||
| 			try { | ||||
| 				const { data } = await api.assetApi.serveFile(assetInfo.deviceAssetId, deviceId, false, true, { | ||||
| 					responseType: 'blob', | ||||
| 				}); | ||||
|  | ||||
| 			const assetData = URL.createObjectURL(await res.blob()); | ||||
| 				if (!(data instanceof Blob)) { | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 			return assetData; | ||||
| 				const assetData = URL.createObjectURL(data); | ||||
| 				return assetData; | ||||
| 			} catch (e) {} | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
|   | ||||
| @@ -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,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'; | ||||
|  | ||||
|   | ||||
| @@ -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,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'; | ||||
|   | ||||
| @@ -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,4 +1,4 @@ | ||||
| import { serverEndpoint } from './constants'; | ||||
| import { serverEndpoint } from '../constants'; | ||||
| 
 | ||||
| type ISend = { | ||||
| 	method: string; | ||||
| @@ -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', | ||||
| 			}, | ||||
|  | ||||
| 		const { data, status } = await api.assetApi.checkDuplicateAsset({ | ||||
| 			deviceAssetId: String(deviceAssetId), | ||||
| 			deviceId: 'WEB', | ||||
| 		}); | ||||
|  | ||||
| 		if (res.status === 200) { | ||||
| 			const { isExist } = await res.json(); | ||||
|  | ||||
| 			if (isExist) { | ||||
| 		if (status === 200) { | ||||
| 			if (data.isExist) { | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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', | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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') { | ||||
|   | ||||
| @@ -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', | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -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 }, | ||||
| 	}; | ||||
| }; | ||||
| @@ -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! } }, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -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,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', | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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', | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -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(); | ||||
| 	export const load: Load = async ({ session }) => { | ||||
| 		const { data } = await api.userApi.getUserCount(); | ||||
|  | ||||
| 		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 { | ||||
| 		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; | ||||
|   | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -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'), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
|   | ||||
| @@ -17,6 +17,11 @@ | ||||
|     "strict": true, | ||||
|     "target": "es2020", | ||||
|     "importsNotUsedAsValues": "preserve", | ||||
|     "preserveValueImports": false | ||||
|     "preserveValueImports": false, | ||||
|     "paths": { | ||||
|       "$lib": ["src/lib"], | ||||
|       "$lib/*": ["src/lib/*"], | ||||
|       "@api": ["src/api"] | ||||
|     } | ||||
|   }, | ||||
| } | ||||
		Reference in New Issue
	
	Block a user