1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02: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:
Alex 2022-07-10 21:41:45 -05:00 committed by GitHub
parent 7f236c5b18
commit 9a6dfacf9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 516 additions and 691 deletions

View File

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

View File

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

View File

@ -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;
}

View File

@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class ServeFileDto {
@IsNotEmpty() @IsNotEmpty()
@ -11,10 +12,28 @@ export class ServeFileDto {
did!: string; did!: string;
@IsOptional() @IsOptional()
@IsBooleanString() @IsBoolean()
isThumb?: string; @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() @IsOptional()
@IsBooleanString() @IsBoolean()
isWeb?: string; @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;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ describe('User', () => {
}); });
it('fetches the user collection excluding the auth user', async () => { 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(status).toEqual(200);
expect(body).toHaveLength(2); expect(body).toHaveLength(2);
expect(body).toEqual( expect(body).toEqual(

File diff suppressed because one or more lines are too long

View File

@ -53,7 +53,7 @@
"@nestjs/cli": "^8.2.8", "@nestjs/cli": "^8.2.8",
"@nestjs/schematics": "^8.0.11", "@nestjs/schematics": "^8.0.11",
"@nestjs/testing": "^8.4.7", "@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/bcrypt": "^5.0.0",
"@types/bull": "^3.15.7", "@types/bull": "^3.15.7",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",

View File

@ -23,7 +23,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "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", "test:e2e": "jest --config ./apps/immich/test/jest-e2e.json",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", "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": { "dependencies": {
"@mapbox/mapbox-sdk": "^0.13.3", "@mapbox/mapbox-sdk": "^0.13.3",

View File

@ -1,3 +1,4 @@
import { serverEndpoint } from '$lib/constants';
import { import {
AlbumApi, AlbumApi,
AssetApi, AssetApi,
@ -6,7 +7,7 @@ import {
DeviceInfoApi, DeviceInfoApi,
ServerInfoApi, ServerInfoApi,
UserApi, UserApi,
} from '../open-api'; } from './open-api';
class ImmichApi { class ImmichApi {
public userApi: UserApi; public userApi: UserApi;
@ -15,7 +16,7 @@ class ImmichApi {
public authenticationApi: AuthenticationApi; public authenticationApi: AuthenticationApi;
public deviceInfoApi: DeviceInfoApi; public deviceInfoApi: DeviceInfoApi;
public serverInfoApi: ServerInfoApi; public serverInfoApi: ServerInfoApi;
private config = new Configuration(); private config = new Configuration({ basePath: serverEndpoint });
constructor() { constructor() {
this.userApi = new UserApi(this.config); 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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import type { ImmichUser } from '$lib/models/immich-user'; import type { ImmichUser } from '$lib/models/immich-user';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { postRequest } from '../../api';
import { serverEndpoint } from '../../constants'; import { serverEndpoint } from '../../constants';
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte'; import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
import { clickOutside } from './click-outside'; import { clickOutside } from './click-outside';
import { api } from '@api';
export let user: ImmichUser; export let user: ImmichUser;
@ -16,12 +17,22 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let shouldShowAccountInfoPanel = false; 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) => { const getFirstLetter = (text?: string) => {
return text?.charAt(0).toUpperCase(); return text?.charAt(0).toUpperCase();
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,26 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { serverEndpoint } from '$lib/constants'; import { api } from '@api';
export const post: RequestHandler = async ({ request, locals }) => { 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 password = form.get('password');
const payload = { const { status } = await api.userApi.updateUser({
id: locals.user?.id, id: locals.user.id,
password, password: String(password),
shouldChangePassword: false, 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 { return {
status: 200, status: 200,
body: { body: {
@ -32,7 +31,7 @@ export const post: RequestHandler = async ({ request, locals }) => {
return { return {
status: 400, status: 400,
body: { body: {
error: await res.json(), error: 'Error change password',
}, },
}; };
} }

View File

@ -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 },
};
};

View File

@ -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! } },
}
}
}

View File

@ -1,17 +1,6 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { serverEndpoint } from '$lib/constants';
import * as cookie from 'cookie'; import * as cookie from 'cookie';
import { getRequest, putRequest } from '$lib/api'; import { api } from '@api';
type AuthUser = {
accessToken: string;
userId: string;
userEmail: string;
firstName: string;
lastName: string;
isAdmin: boolean;
shouldChangePassword: boolean;
};
export const post: RequestHandler = async ({ request }) => { export const post: RequestHandler = async ({ request }) => {
const form = await request.formData(); const form = await request.formData();
@ -19,22 +8,11 @@ export const post: RequestHandler = async ({ request }) => {
const email = form.get('email'); const email = form.get('email');
const password = form.get('password'); const password = form.get('password');
const payload = { try {
email, const { data: authUser } = await api.authenticationApi.login({
password, email: String(email),
}; password: String(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;
return { return {
status: 200, status: 200,
@ -70,7 +48,7 @@ export const post: RequestHandler = async ({ request }) => {
), ),
}, },
}; };
} else { } catch (error) {
return { return {
status: 400, status: 400,
body: { body: {

View File

@ -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',
},
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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