Add OpenAPI Specs and Response DTOs (#320)

* Added swagger bearer auth method authentication accordingly

* Update Auth endpoint

* Added additional api information for authentication

* Added Swagger CLI pluggin

* Added DTO for /user endpoint

* Added /device-info reponse DTOs

* Implement server version

* Added DTOs for /server-info

* Added DTOs for /assets

* Added album to Swagger group

* Added generated specs file

* Add Client API generator for web

* Remove incorrectly placed node_modules

* Created class to handle access token

* Remove password and hash when getting all user

* PR feedback

* Fixed video from CLI doesn't get metadata extracted

* Fixed issue with TSConfig to work with generated openAPI

* PR feedback

* Remove console.log
This commit is contained in:
Alex 2022-07-08 21:26:50 -05:00 committed by GitHub
parent 25985c732d
commit 7f236c5b18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 5477 additions and 226 deletions

View file

@ -22,20 +22,23 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiTags('Album')
@Controller('album')
export class AlbumController {
constructor(private readonly albumService: AlbumService) {}
@Post()
async create(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
return this.albumService.create(authUser, createAlbumDto);
}
@Put('/:albumId/users')
async addUsers(
async addUsersToAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) addUsersDto: AddUsersDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
@ -44,7 +47,7 @@ export class AlbumController {
}
@Put('/:albumId/assets')
async addAssets(
async addAssetsToAlbum(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) addAssetsDto: AddAssetsDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,

View file

@ -2,15 +2,15 @@ import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
export interface AlbumResponseDto {
id: string;
ownerId: string;
albumName: string;
createdAt: string;
albumThumbnailAssetId: string | null;
shared: boolean;
sharedUsers: UserResponseDto[];
assets: AssetResponseDto[];
export class AlbumResponseDto {
id!: string;
ownerId!: string;
albumName!: string;
createdAt!: string;
albumThumbnailAssetId!: string | null;
shared!: boolean;
sharedUsers!: UserResponseDto[];
assets!: AssetResponseDto[];
}
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {

View file

@ -36,8 +36,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 { 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';
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiTags('Asset')
@Controller('asset')
export class AssetController {
constructor(
@ -89,7 +95,7 @@ export class AssetController {
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Query(ValidationPipe) query: ServeFileDto,
) {
): Promise<StreamableFile> {
return this.assetService.downloadFile(query, res);
}
@ -109,43 +115,58 @@ export class AssetController {
}
@Get('/allObjects')
async getCuratedObject(@GetAuthUser() authUser: AuthUserDto) {
async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser);
}
@Get('/allLocation')
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser);
}
@Get('/searchTerm')
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<String[]> {
return this.assetService.getAssetSearchTerm(authUser);
}
@Post('/search')
async searchAsset(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) searchAssetDto: SearchAssetDto) {
async searchAsset(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) searchAssetDto: SearchAssetDto,
): Promise<AssetResponseDto[]> {
return this.assetService.searchAsset(authUser, searchAssetDto);
}
/**
* Get all AssetEntity belong to the user
*/
@Get('/')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> {
return await this.assetService.getAllAssets(authUser);
}
/**
* Get all asset of a device that are in the database, ID only.
*/
@Get('/:deviceId')
async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
}
/**
* Get a single asset's information
*/
@Get('/assetById/:assetId')
async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId: string) {
async getAssetById(
@GetAuthUser() authUser: AuthUserDto,
@Param('assetId') assetId: string,
): Promise<AssetResponseDto> {
return await this.assetService.getAssetById(authUser, assetId);
}
@Delete('/')
async deleteAssetById(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
const deleteAssetList: AssetEntity[] = [];
async deleteAsset(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
const deleteAssetList: AssetResponseDto[] = [];
for (const id of assetIds.ids) {
const assets = await this.assetService.getAssetById(authUser, id);

View file

@ -19,6 +19,8 @@ import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
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';
const fileInfo = promisify(stat);
@ -80,9 +82,8 @@ export class AssetService {
return res;
}
public async getAllAssets(authUser: AuthUserDto) {
try {
return await this.assetRepository.find({
public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.find({
where: {
userId: authUser.id,
resizePath: Not(IsNull()),
@ -92,37 +93,44 @@ export class AssetService {
createdAt: 'DESC',
},
});
} catch (e) {
Logger.error(e, 'getAllAssets');
}
return assets.map((asset) => mapAsset(asset));
}
public async findOne(deviceId: string, assetId: string): Promise<AssetEntity> {
public async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
const rows = await this.assetRepository.query(
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2',
[assetId, deviceId],
);
if (rows.lengh == 0) {
throw new BadRequestException('Not Found');
throw new NotFoundException('Not Found');
}
return rows[0] as AssetEntity;
const assetOnDevice = rows[0] as AssetEntity;
return mapAsset(assetOnDevice);
}
public async getAssetById(authUser: AuthUserDto, assetId: string) {
return await this.assetRepository.findOne({
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
const asset = await this.assetRepository.findOne({
where: {
id: assetId,
},
relations: ['exifInfo'],
});
if (!asset) {
throw new NotFoundException('Asset not found');
}
return mapAsset(asset);
}
public async downloadFile(query: ServeFileDto, res: Res) {
try {
let fileReadStream = null;
const asset = await this.findOne(query.did, query.aid);
const asset = await this.findAssetOfDevice(query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) {
const { size } = await fileInfo(asset.originalPath);
@ -188,7 +196,7 @@ export class AssetService {
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
let fileReadStream: ReadStream;
const asset = await this.findOne(query.did, query.aid);
const asset = await this.findAssetOfDevice(query.did, query.aid);
if (!asset) {
throw new NotFoundException('Asset does not exist');
@ -258,12 +266,13 @@ export class AssetService {
try {
// Handle Video
let videoPath = asset.originalPath;
let mimeType = asset.mimeType;
await fs.access(videoPath, constants.R_OK | constants.W_OK);
if (query.isWeb && asset.mimeType == 'video/quicktime') {
videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath);
mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
}
@ -390,7 +399,7 @@ export class AssetService {
return Array.from(possibleSearchTerm).filter((x) => x != null);
}
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto) {
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> {
const query = `
SELECT a.*
FROM assets a
@ -406,7 +415,12 @@ export class AssetService {
);
`;
return await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
const searchResults: AssetEntity[] = await this.assetRepository.query(query, [
authUser.id,
searchAssetDto.searchTerm,
]);
return searchResults.map((asset) => mapAsset(asset));
}
async getCuratedLocation(authUser: AuthUserDto) {
@ -423,8 +437,8 @@ export class AssetService {
);
}
async getCuratedObject(authUser: AuthUserDto) {
return await this.assetRepository.query(
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query(
`
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
@ -434,9 +448,11 @@ export class AssetService {
`,
[authUser.id],
);
return curatedObjects;
}
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto) {
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto): Promise<boolean> {
const res = await this.assetRepository.findOne({
where: {
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,

View file

@ -1,6 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class DeleteAssetDto {
@IsNotEmpty()
@ApiProperty({
isArray: true,
type: String,
title: 'Array of asset IDs to delete',
example: [
'bf973405-3f2a-48d2-a687-2ed4167164be',
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
],
})
ids!: string[];
}

View file

@ -1,12 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
export class ServeFileDto {
//assetId
@IsNotEmpty()
@ApiProperty({ type: String, title: 'Device Asset ID' })
aid!: string;
//deviceId
@IsNotEmpty()
@ApiProperty({ type: String, title: 'Device ID' })
did!: string;
@IsOptional()

View file

@ -2,19 +2,21 @@ import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { ExifResponseDto, mapExif } from './exif-response.dto';
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
export interface AssetResponseDto {
id: string;
deviceAssetId: string;
ownerId: string;
deviceId: string;
type: AssetType;
originalPath: string;
resizePath: string | null;
createdAt: string;
modifiedAt: string;
isFavorite: boolean;
mimeType: string | null;
duration: string;
export class AssetResponseDto {
id!: string;
deviceAssetId!: string;
ownerId!: string;
deviceId!: string;
type!: AssetType;
originalPath!: string;
resizePath!: string | null;
createdAt!: string;
modifiedAt!: string;
isFavorite!: boolean;
mimeType!: string | null;
duration!: string;
webpPath!: string | null;
encodedVideoPath!: string | null;
exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto;
}
@ -32,6 +34,8 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
modifiedAt: entity.modifiedAt,
isFavorite: entity.isFavorite,
mimeType: entity.mimeType,
webpPath: entity.webpPath,
encodedVideoPath: entity.encodedVideoPath,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,

View file

@ -0,0 +1,7 @@
export class CuratedLocationsResponseDto {
id!: string;
city!: string;
resizePath!: string;
deviceAssetId!: string;
deviceId!: string;
}

View file

@ -0,0 +1,7 @@
export class CuratedObjectsResponseDto {
id!: string;
object!: string;
resizePath!: string;
deviceAssetId!: string;
deviceId!: string;
}

View file

@ -1,26 +1,26 @@
import { ExifEntity } from '@app/database/entities/exif.entity';
export interface ExifResponseDto {
id: string;
make: string | null;
model: string | null;
imageName: string | null;
exifImageWidth: number | null;
exifImageHeight: number | null;
fileSizeInByte: number | null;
orientation: string | null;
dateTimeOriginal: Date | null;
modifyDate: Date | null;
lensModel: string | null;
fNumber: number | null;
focalLength: number | null;
iso: number | null;
exposureTime: number | null;
latitude: number | null;
longitude: number | null;
city: string | null;
state: string | null;
country: string | null;
export class ExifResponseDto {
id!: string;
make: string | null = null;
model: string | null = null;
imageName: string | null = null;
exifImageWidth: number | null = null;
exifImageHeight: number | null = null;
fileSizeInByte: number | null = null;
orientation: string | null = null;
dateTimeOriginal: Date | null = null;
modifyDate: Date | null = null;
lensModel: string | null = null;
fNumber: number | null = null;
focalLength: number | null = null;
iso: number | null = null;
exposureTime: number | null = null;
latitude: number | null = null;
longitude: number | null = null;
city: string | null = null;
state: string | null = null;
country: string | null = null;
}
export function mapExif(entity: ExifEntity): ExifResponseDto {

View file

@ -1,9 +1,9 @@
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
export interface SmartInfoResponseDto {
id: string;
tags: string[] | null;
objects: string[] | null;
export class SmartInfoResponseDto {
id?: string;
tags?: string[] | null;
objects?: string[] | null;
}
export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {

View file

@ -1,30 +1,42 @@
import { Body, Controller, Post, UseGuards, ValidationPipe } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiCreatedResponse,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AuthService } from './auth.service';
import { LoginCredentialDto } from './dto/login-credential.dto';
import { LoginResponseDto } from './response-dto/login-response.dto';
import { SignUpDto } from './dto/sign-up.dto';
import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,';
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('/login')
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
return await this.authService.login(loginCredential);
}
@Post('/admin-sign-up')
async adminSignUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
return await this.authService.adminSignUp(signUpCrendential);
@ApiBadRequestResponse({ description: 'The server already has an admin' })
async adminSignUp(@Body(ValidationPipe) signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
return await this.authService.adminSignUp(signUpCredential);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Post('/validateToken')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async validateToken(@GetAuthUser() authUser: AuthUserDto) {
return {
authStatus: true,
};
async validateAccessToken(@GetAuthUser() authUser: AuthUserDto): Promise<ValidateAccessTokenResponseDto> {
return new ValidateAccessTokenResponseDto(true);
}
}

View file

@ -7,7 +7,8 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtPayloadDto } from './dto/jwt-payload.dto';
import { SignUpDto } from './dto/sign-up.dto';
import * as bcrypt from 'bcrypt';
import { mapUser, UserResponseDto } from '../user/response-dto/user-response.dto';
import { LoginResponseDto, mapLoginResponse } from './response-dto/login-response.dto';
import { AdminSignupResponseDto, mapAdminSignupResponse } from './response-dto/admin-signup-response.dto';
@Injectable()
export class AuthService {
@ -49,7 +50,7 @@ export class AuthService {
return null;
}
public async login(loginCredential: LoginCredentialDto) {
public async login(loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
const validatedUser = await this.validateUser(loginCredential);
if (!validatedUser) {
@ -57,20 +58,12 @@ export class AuthService {
}
const payload = new JwtPayloadDto(validatedUser.id, validatedUser.email);
const accessToken = await this.immichJwtService.generateToken(payload);
return {
accessToken: await this.immichJwtService.generateToken(payload),
userId: validatedUser.id,
userEmail: validatedUser.email,
firstName: validatedUser.firstName,
lastName: validatedUser.lastName,
isAdmin: validatedUser.isAdmin,
profileImagePath: validatedUser.profileImagePath,
shouldChangePassword: validatedUser.shouldChangePassword,
};
return mapLoginResponse(validatedUser, accessToken);
}
public async adminSignUp(signUpCredential: SignUpDto): Promise<UserResponseDto> {
public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
if (adminUser) {
@ -88,7 +81,7 @@ export class AuthService {
try {
const savedNewAdminUserUser = await this.userRepository.save(newAdminUser);
return mapUser(savedNewAdminUserUser);
return mapAdminSignupResponse(savedNewAdminUserUser);
} catch (e) {
Logger.error('e', 'signUp');
throw new InternalServerErrorException('Failed to register new admin user');

View file

@ -1,9 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class LoginCredentialDto {
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
}

View file

@ -1,15 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class SignUpDto {
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsNotEmpty()
@ApiProperty({ example: 'Admin' })
firstName!: string;
@IsNotEmpty()
@ApiProperty({ example: 'Doe' })
lastName!: string;
}

View file

@ -0,0 +1,19 @@
import { UserEntity } from '@app/database/entities/user.entity';
export class AdminSignupResponseDto {
id!: string;
email!: string;
firstName!: string;
lastName!: string;
createdAt!: string;
}
export function mapAdminSignupResponse(entity: UserEntity): AdminSignupResponseDto {
return {
id: entity.id,
email: entity.email,
firstName: entity.firstName,
lastName: entity.lastName,
createdAt: entity.createdAt,
};
}

View file

@ -0,0 +1,41 @@
import { UserEntity } from '@app/database/entities/user.entity';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
export class LoginResponseDto {
@ApiResponseProperty()
accessToken!: string;
@ApiResponseProperty()
userId!: string;
@ApiResponseProperty()
userEmail!: string;
@ApiResponseProperty()
firstName!: string;
@ApiResponseProperty()
lastName!: string;
@ApiResponseProperty()
profileImagePath!: string;
@ApiResponseProperty()
isAdmin!: boolean;
@ApiResponseProperty()
shouldChangePassword!: boolean;
}
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
return {
accessToken: accessToken,
userId: entity.id,
userEmail: entity.email,
firstName: entity.firstName,
lastName: entity.lastName,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
};
}

View file

@ -0,0 +1,7 @@
export class ValidateAccessTokenResponseDto {
constructor(authStatus: boolean) {
this.authStatus = authStatus;
}
authStatus: boolean;
}

View file

@ -1,22 +1,32 @@
import { Controller, Post, Body, Patch, UseGuards, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { DeviceInfoService } from './device-info.service';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoResponseDto } from './response-dto/create-device-info-response.dto';
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiTags('Device Info')
@Controller('device-info')
export class DeviceInfoController {
constructor(private readonly deviceInfoService: DeviceInfoService) {}
@Post()
async create(@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
return await this.deviceInfoService.create(createDeviceInfoDto, authUser);
async createDeviceInfo(
@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto,
@GetAuthUser() authUser: AuthUserDto,
): Promise<DeviceInfoResponseDto> {
return this.deviceInfoService.create(createDeviceInfoDto, authUser);
}
@Patch()
async update(@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
async updateDeviceInfo(
@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto,
@GetAuthUser() authUser: AuthUserDto,
): Promise<DeviceInfoResponseDto> {
return this.deviceInfoService.update(authUser.id, updateDeviceInfoDto);
}
}

View file

@ -1,10 +1,11 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto/create-device-info-response.dto';
@Injectable()
export class DeviceInfoService {
@ -13,7 +14,7 @@ export class DeviceInfoService {
private deviceRepository: Repository<DeviceInfoEntity>,
) {}
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto) {
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto): Promise<DeviceInfoResponseDto> {
const res = await this.deviceRepository.findOne({
where: {
deviceId: createDeviceInfoDto.deviceId,
@ -23,7 +24,7 @@ export class DeviceInfoService {
if (res) {
Logger.log('Device Info Exist', 'createDeviceInfo');
return res;
return mapDeviceInfoResponse(res);
}
const deviceInfo = new DeviceInfoEntity();
@ -31,20 +32,18 @@ export class DeviceInfoService {
deviceInfo.deviceType = createDeviceInfoDto.deviceType;
deviceInfo.userId = authUser.id;
try {
return await this.deviceRepository.save(deviceInfo);
} catch (e) {
Logger.error('Error creating new device info', 'createDeviceInfo');
}
const newDeviceInfo = await this.deviceRepository.save(deviceInfo);
return mapDeviceInfoResponse(newDeviceInfo);
}
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto) {
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto): Promise<DeviceInfoResponseDto> {
const deviceInfo = await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
if (!deviceInfo) {
throw new BadRequestException('Device Not Found');
throw new NotFoundException('Device Not Found');
}
const res = await this.deviceRepository.update(
@ -55,9 +54,15 @@ export class DeviceInfoService {
);
if (res.affected == 1) {
return await this.deviceRepository.findOne({
const updatedDeviceInfo = await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
if (!updatedDeviceInfo) {
throw new NotFoundException('Device Not Found');
}
return mapDeviceInfoResponse(updatedDeviceInfo);
} else {
throw new BadRequestException('Bad Request');
}

View file

@ -0,0 +1,23 @@
import { DeviceInfoEntity, DeviceType } from '@app/database/entities/device-info.entity';
export class DeviceInfoResponseDto {
id!: number;
userId!: string;
deviceId!: string;
deviceType!: DeviceType;
notificationToken!: string | null;
createdAt!: string;
isAutoBackup!: boolean;
}
export function mapDeviceInfoResponse(entity: DeviceInfoEntity): DeviceInfoResponseDto {
return {
id: entity.id,
userId: entity.userId,
deviceId: entity.deviceId,
deviceType: entity.deviceType,
notificationToken: entity.notificationToken,
createdAt: entity.createdAt,
isAutoBackup: entity.isAutoBackup,
};
}

View file

@ -1,5 +1,4 @@
// TODO: this is being used as a response DTO. Should be changed to interface
export class ServerInfoDto {
export class ServerInfoResponseDto {
diskSize!: string;
diskUse!: string;
diskAvailable!: string;

View file

@ -0,0 +1,10 @@
import { ApiResponseProperty } from '@nestjs/swagger';
export class ServerPingResponse {
constructor(res: string) {
this.res = res;
}
@ApiResponseProperty({ type: String, example: 'pong' })
res!: string;
}

View file

@ -0,0 +1,8 @@
import { IServerVersion } from 'apps/immich/src/constants/server_version.constant';
export class ServerVersionReponseDto implements IServerVersion {
major!: number;
minor!: number;
patch!: number;
build!: number;
}

View file

@ -3,34 +3,28 @@ import { ConfigService } from '@nestjs/config';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { ServerInfoService } from './server-info.service';
import { serverVersion } from '../../constants/server_version.constant';
import { ApiTags } from '@nestjs/swagger';
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
@ApiTags('Server Info')
@Controller('server-info')
export class ServerInfoController {
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {}
@Get()
async getServerInfo() {
async getServerInfo(): Promise<ServerInfoResponseDto> {
return await this.serverInfoService.getServerInfo();
}
@Get('/ping')
async getServerPulse() {
return {
res: 'pong',
};
}
@UseGuards(JwtAuthGuard)
@Get('/mapbox')
async getMapboxInfo() {
return {
isEnable: this.configService.get('ENABLE_MAPBOX'),
mapboxSecret: this.configService.get('MAPBOX_KEY'),
};
async pingServer(): Promise<ServerPingResponse> {
return new ServerPingResponse('pong');
}
@Get('/version')
async getServerVersion() {
async getServerVersion(): Promise<ServerVersionReponseDto> {
return serverVersion;
}
}

View file

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { ServerInfoDto } from './dto/server-info.dto';
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
import diskusage from 'diskusage';
import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
@ -10,7 +10,7 @@ export class ServerInfoService {
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
const serverInfo = new ServerInfoDto();
const serverInfo = new ServerInfoResponseDto();
serverInfo.diskAvailable = ServerInfoService.getHumanReadableString(diskInfo.available);
serverInfo.diskSize = ServerInfoService.getHumanReadableString(diskInfo.total);
serverInfo.diskUse = ServerInfoService.getHumanReadableString(diskInfo.total - diskInfo.free);

View file

@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class CreateProfileImageDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: any;
}

View file

@ -1,27 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsNotEmpty()
@ApiProperty({ example: 'John' })
firstName!: string;
@IsNotEmpty()
@ApiProperty({ example: 'Doe' })
lastName!: string;
@IsOptional()
profileImagePath?: string;
@IsOptional()
isAdmin?: boolean;
@IsOptional()
shouldChangePassword?: boolean;
@IsOptional()
id?: string;
}

View file

@ -1,4 +1,24 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { IsNotEmpty, IsOptional } from 'class-validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
export class UpdateUserDto {
@IsNotEmpty()
id!: string;
@IsOptional()
password?: string;
@IsOptional()
firstName?: string;
@IsOptional()
lastName?: string;
@IsOptional()
isAdmin?: boolean;
@IsOptional()
shouldChangePassword?: boolean;
@IsOptional()
profileImagePath?: string;
}

View file

@ -0,0 +1,11 @@
export class CreateProfileImageResponseDto {
userId!: string;
profileImagePath!: string;
}
export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
return {
userId: userId,
profileImagePath: profileImagePath,
};
}

View file

@ -0,0 +1,10 @@
export class UserCountResponseDto {
userCount!: number;
}
export function mapUserCountResponse(count: number): UserCountResponseDto {
return {
userCount: count,
};
}

View file

@ -1,11 +1,14 @@
import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity';
import { UserEntity } from '@app/database/entities/user.entity';
export interface UserResponseDto {
id: string;
email: string;
firstName: string;
lastName: string;
createdAt: string;
export class UserResponseDto {
id!: string;
email!: string;
firstName!: string;
lastName!: string;
createdAt!: string;
profileImagePath!: string;
shouldChangePassword!: boolean;
isAdmin!: boolean;
}
export function mapUser(entity: UserEntity): UserResponseDto {
@ -15,5 +18,8 @@ export function mapUser(entity: UserEntity): UserResponseDto {
firstName: entity.firstName,
lastName: entity.lastName,
createdAt: entity.createdAt,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
isAdmin: entity.isAdmin,
};
}

View file

@ -11,6 +11,7 @@ import {
UseInterceptors,
UploadedFile,
Response,
StreamableFile,
} from '@nestjs/common';
import { UserService } from './user.service';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
@ -21,50 +22,72 @@ import { UpdateUserDto } from './dto/update-user.dto';
import { FileInterceptor } from '@nestjs/platform-express';
import { profileImageUploadOption } from '../../config/profile-image-upload.config';
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';
@ApiTags('User')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get()
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean) {
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return await this.userService.getAllUsers(authUser, isAll);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('me')
async getUserInfo(@GetAuthUser() authUser: AuthUserDto) {
async getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return await this.userService.getUserInfo(authUser);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@UseGuards(AdminRolesGuard)
@Post()
async createNewUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto): Promise<UserResponseDto> {
return await this.userService.createUser(createUserDto);
}
@Get('/count')
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
async getUserCount(@Query('isAdmin') isAdmin: boolean): Promise<UserCountResponseDto> {
return await this.userService.getUserCount(isAdmin);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Put()
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
return await this.userService.updateUser(updateUserDto);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiConsumes('multipart/form-data')
@ApiBody({
type: CreateProfileImageDto,
})
@Post('/profile-image')
async createProfileImage(@GetAuthUser() authUser: AuthUserDto, @UploadedFile() fileInfo: Express.Multer.File) {
async createProfileImage(
@GetAuthUser() authUser: AuthUserDto,
@UploadedFile() fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> {
return await this.userService.createProfileImage(authUser, fileInfo);
}
@Get('/profile-image/:userId')
async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res) {
return await this.userService.getUserProfileImage(userId, res);
async getProfileImage(
@Param('userId') userId: string,
@Response({ passthrough: true }) res: Res,
): Promise<StreamableFile | undefined> {
return this.userService.getUserProfileImage(userId, res);
}
}

View file

@ -16,6 +16,11 @@ import * as bcrypt from 'bcrypt';
import { createReadStream } from 'fs';
import { Response as Res } from 'express';
import { mapUser, UserResponseDto } from './response-dto/user-response.dto';
import { mapUserCountResponse, UserCountResponseDto } from './response-dto/user-count-response.dto';
import {
CreateProfileImageResponseDto,
mapCreateProfileImageResponse,
} from './response-dto/create-profile-image-response.dto';
@Injectable()
export class UserService {
@ -24,24 +29,32 @@ export class UserService {
private userRepository: Repository<UserEntity>,
) {}
async getAllUsers(authUser: AuthUserDto, isAll: boolean) {
async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
if (isAll) {
return await this.userRepository.find();
const allUsers = await this.userRepository.find();
return allUsers.map(mapUser);
}
return await this.userRepository.find({
const allUserExceptRequestedUser = await this.userRepository.find({
where: { id: Not(authUser.id) },
order: {
createdAt: 'DESC',
},
});
return allUserExceptRequestedUser.map(mapUser);
}
async getUserInfo(authUser: AuthUserDto) {
return this.userRepository.findOne({ where: { id: authUser.id } });
async getUserInfo(authUser: AuthUserDto): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id: authUser.id } });
if (!user) {
throw new BadRequestException('User not found');
}
return mapUser(user);
}
async getUserCount(isAdmin: boolean) {
async getUserCount(isAdmin: boolean): Promise<UserCountResponseDto> {
let users;
if (isAdmin) {
@ -50,9 +63,7 @@ export class UserService {
users = await this.userRepository.find();
}
return {
userCount: users.length,
};
return mapUserCountResponse(users.length);
}
async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> {
@ -84,7 +95,7 @@ export class UserService {
return bcrypt.hash(password, salt);
}
async updateUser(updateUserDto: UpdateUserDto) {
async updateUser(updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id: updateUserDto.id } });
if (!user) {
throw new NotFoundException('User not found');
@ -115,31 +126,23 @@ export class UserService {
try {
const updatedUser = await this.userRepository.save(user);
// TODO: this should probably retrun UserResponseDto
return {
id: updatedUser.id,
email: updatedUser.email,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
isAdmin: updatedUser.isAdmin,
profileImagePath: updatedUser.profileImagePath,
};
return mapUser(updatedUser);
} catch (e) {
Logger.error(e, 'Create new user');
throw new InternalServerErrorException('Failed to register new user');
}
}
async createProfileImage(authUser: AuthUserDto, fileInfo: Express.Multer.File) {
async createProfileImage(
authUser: AuthUserDto,
fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> {
try {
await this.userRepository.update(authUser.id, {
profileImagePath: fileInfo.path,
});
return {
userId: authUser.id,
profileImagePath: fileInfo.path,
};
return mapCreateProfileImageResponse(authUser.id, fileInfo.path);
} catch (e) {
Logger.error(e, 'Create User Profile Image');
throw new InternalServerErrorException('Failed to create new user profile image');

View file

@ -1,7 +1,14 @@
// major.minor.patch+build
// check mobile/pubspec.yml for current release version
export const serverVersion = {
export interface IServerVersion {
major: number;
minor: number;
patch: number;
build: number;
}
export const serverVersion: IServerVersion = {
major: 1,
minor: 17,
patch: 0,

View file

@ -1,6 +1,9 @@
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
import { writeFileSync } from 'fs';
import path from 'path';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
@ -15,6 +18,38 @@ async function bootstrap() {
app.useWebSocketAdapter(new RedisIoAdapter(app));
const config = new DocumentBuilder()
.setTitle('Immich')
.setDescription('Immich API')
.setVersion('1.17.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
})
.addServer('/api')
.build();
const apiDocumentOptions: SwaggerDocumentOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
};
const apiDocument = SwaggerModule.createDocument(app, config, apiDocumentOptions);
SwaggerModule.setup('doc', app, apiDocument, {
swaggerOptions: {
persistAuthorization: true,
},
customSiteTitle: 'Immich API Documentation',
});
// Generate API Documentation
const outputPath = path.resolve(process.cwd(), 'immich-openapi-specs.json');
writeFileSync(outputPath, JSON.stringify(apiDocument), { encoding: 'utf8' });
await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log('Running Immich Server in DEVELOPMENT environment', 'ImmichServer');

View file

@ -5,6 +5,7 @@ import { AssetEntity } from '@app/database/entities/asset.entity';
import fs from 'fs';
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
import { Job } from 'bull';
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
@Processor('background-task')
export class BackgroundTaskProcessor {
@ -18,7 +19,7 @@ export class BackgroundTaskProcessor {
// TODO: Should probably use constants / Interfaces for Queue names / data
@Process('delete-file-on-disk')
async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) {
async deleteFileOnDisk(job: Job<{ assets: AssetResponseDto[] }>) {
const { assets } = job.data;
for (const asset of assets) {

View file

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { randomUUID } from 'node:crypto';
import { AssetEntity } from '@app/database/entities/asset.entity';
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
@Injectable()
export class BackgroundTaskService {
@ -11,7 +12,7 @@ export class BackgroundTaskService {
private backgroundTaskQueue: Queue,
) {}
async deleteFileOnDisk(assets: AssetEntity[]) {
async deleteFileOnDisk(assets: AssetResponseDto[]) {
await this.backgroundTaskQueue.add(
'delete-file-on-disk',
{

View file

@ -63,7 +63,7 @@ export class AssetUploadedProcessor {
}
// Extract video duration if uploaded from the web & CLI
if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
if (asset.type == AssetType.VIDEO) {
await this.metadataExtractionQueue.add(videoMetadataExtractionProcessorName, { asset }, { jobId: randomUUID() });
}
}

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,14 @@
"root": "apps/immich",
"compilerOptions": {
"webpack": false,
"tsConfigPath": "apps/immich/tsconfig.app.json"
"tsConfigPath": "apps/immich/tsconfig.app.json",
"plugins": [ {
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": false,
"introspectComments": true
}
}]
},
"projects": {
"immich": {

7
server/openapitools.json Normal file
View file

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.0.1"
}
}

594
server/package-lock.json generated
View file

@ -21,6 +21,7 @@
"@nestjs/platform-fastify": "^8.4.7",
"@nestjs/platform-socket.io": "^8.4.7",
"@nestjs/schedule": "^2.0.1",
"@nestjs/swagger": "^5.2.1",
"@nestjs/typeorm": "^8.1.4",
"@nestjs/websockets": "^8.4.7",
"@socket.io/redis-adapter": "^7.1.0",
@ -44,6 +45,7 @@
"rxjs": "^7.2.0",
"sharp": "^0.28.0",
"socket.io-redis": "^6.1.1",
"swagger-ui-express": "^4.4.0",
"systeminformation": "^5.11.0",
"typeorm": "^0.3.6"
},
@ -51,6 +53,7 @@
"@nestjs/cli": "^8.2.8",
"@nestjs/schematics": "^8.0.11",
"@nestjs/testing": "^8.4.7",
"@openapitools/openapi-generator-cli": "^2.5.1",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.7",
"@types/cron": "^2.0.0",
@ -1839,6 +1842,31 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/@nestjs/swagger": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-5.2.1.tgz",
"integrity": "sha512-7dNa08WCnTsW/oAk3Ujde+z64JMfNm19DhpXasFR8oJp/9pggYAbYU927HpA+GJsSFJX6adjIRZsCKUqaGWznw==",
"dependencies": {
"@nestjs/mapped-types": "1.0.1",
"lodash": "4.17.21",
"path-to-regexp": "3.2.0"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"fastify-swagger": "*",
"reflect-metadata": "^0.1.12",
"swagger-ui-express": "*"
},
"peerDependenciesMeta": {
"fastify-swagger": {
"optional": true
},
"swagger-ui-express": {
"optional": true
}
}
},
"node_modules/@nestjs/testing": {
"version": "8.4.7",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.4.7.tgz",
@ -1966,6 +1994,214 @@
"npm": ">=5.0.0"
}
},
"node_modules/@openapitools/openapi-generator-cli": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.5.1.tgz",
"integrity": "sha512-WSRQBU0dCSVD+0Qv8iCsv0C4iMaZe/NpJ/CT4SmrEYLH3txoKTE8wEfbdj/kqShS8Or0YEGDPUzhSIKY151L0w==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@nestjs/common": "8.4.4",
"@nestjs/core": "8.4.4",
"@nuxtjs/opencollective": "0.3.2",
"chalk": "4.1.2",
"commander": "8.3.0",
"compare-versions": "4.1.3",
"concurrently": "6.5.1",
"console.table": "0.10.0",
"fs-extra": "10.0.1",
"glob": "7.1.6",
"inquirer": "8.2.2",
"lodash": "4.17.21",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.5",
"tslib": "2.0.3"
},
"bin": {
"openapi-generator-cli": "main.js"
},
"engines": {
"node": ">=10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/openapi_generator"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common": {
"version": "8.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.4.tgz",
"integrity": "sha512-QHi7QcgH/5Jinz+SCfIZJkFHc6Cch1YsAEGFEhi6wSp6MILb0sJMQ1CX06e9tCOAjSlBwaJj4PH0eFCVau5v9Q==",
"dev": true,
"dependencies": {
"axios": "0.26.1",
"iterare": "1.2.1",
"tslib": "2.3.1",
"uuid": "8.3.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"cache-manager": "*",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"cache-manager": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core": {
"version": "8.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.4.4.tgz",
"integrity": "sha512-Ef3yJPuzAttpNfehnGqIV5kHIL9SHptB5F4ERxoU7pT61H3xiYpZw6hSjx68cJO7cc6rm7/N+b4zeuJvFHtvBg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/opencollective": "0.3.2",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
"object-hash": "3.0.0",
"path-to-regexp": "3.2.0",
"tslib": "2.3.1",
"uuid": "8.3.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/microservices": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/websockets": "^8.0.0",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"@nestjs/microservices": {
"optional": true
},
"@nestjs/platform-express": {
"optional": true
},
"@nestjs/websockets": {
"optional": true
}
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"engines": {
"node": ">= 12"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/fs-extra": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
"integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/inquirer": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz",
"integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==",
"dev": true,
"dependencies": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.1",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.21",
"mute-stream": "0.0.8",
"ora": "^5.4.1",
"run-async": "^2.4.0",
"rxjs": "^7.5.5",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/rxjs/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
},
"node_modules/@openapitools/openapi-generator-cli/node_modules/tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
"dev": true
},
"node_modules/@sideway/address": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz",
@ -3247,9 +3483,9 @@
}
},
"node_modules/axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
@ -3989,6 +4225,12 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/compare-versions": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz",
"integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==",
"dev": true
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -4040,6 +4282,61 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/concurrently": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
"integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"rxjs": "^6.6.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^16.2.0"
},
"bin": {
"concurrently": "bin/concurrently.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/concurrently/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/concurrently/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@ -4050,6 +4347,18 @@
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"node_modules/console.table": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz",
"integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==",
"dev": true,
"dependencies": {
"easy-table": "1.1.0"
},
"engines": {
"node": "> 0.10"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -4521,6 +4830,15 @@
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"node_modules/easy-table": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
"integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==",
"dev": true,
"optionalDependencies": {
"wcwidth": ">=1.0.1"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@ -9737,6 +10055,12 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"node_modules/spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
"dev": true
},
"node_modules/spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@ -9996,6 +10320,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.12.0.tgz",
"integrity": "sha512-B0Iy2ueXtbByE6OOyHTi3lFQkpPi/L7kFOKFeKTr44za7dJIELa9kzaca6GkndCgpK1QTjArnoXG+aUy0XQp1w=="
},
"node_modules/swagger-ui-express": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.4.0.tgz",
"integrity": "sha512-1CzRkHG386VQMVZK406jcpgnW2a9A5A/NiAjKhsFTQqUBWRF+uGbXTU/mA7WSV3mTzyOQDvjBdWP/c2qd5lqKw==",
"dependencies": {
"swagger-ui-dist": ">=4.11.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0"
}
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@ -12510,6 +12853,16 @@
}
}
},
"@nestjs/swagger": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-5.2.1.tgz",
"integrity": "sha512-7dNa08WCnTsW/oAk3Ujde+z64JMfNm19DhpXasFR8oJp/9pggYAbYU927HpA+GJsSFJX6adjIRZsCKUqaGWznw==",
"requires": {
"@nestjs/mapped-types": "1.0.1",
"lodash": "4.17.21",
"path-to-regexp": "3.2.0"
}
},
"@nestjs/testing": {
"version": "8.4.7",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-8.4.7.tgz",
@ -12588,6 +12941,150 @@
"node-fetch": "^2.6.1"
}
},
"@openapitools/openapi-generator-cli": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.5.1.tgz",
"integrity": "sha512-WSRQBU0dCSVD+0Qv8iCsv0C4iMaZe/NpJ/CT4SmrEYLH3txoKTE8wEfbdj/kqShS8Or0YEGDPUzhSIKY151L0w==",
"dev": true,
"requires": {
"@nestjs/common": "8.4.4",
"@nestjs/core": "8.4.4",
"@nuxtjs/opencollective": "0.3.2",
"chalk": "4.1.2",
"commander": "8.3.0",
"compare-versions": "4.1.3",
"concurrently": "6.5.1",
"console.table": "0.10.0",
"fs-extra": "10.0.1",
"glob": "7.1.6",
"inquirer": "8.2.2",
"lodash": "4.17.21",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.5",
"tslib": "2.0.3"
},
"dependencies": {
"@nestjs/common": {
"version": "8.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.4.tgz",
"integrity": "sha512-QHi7QcgH/5Jinz+SCfIZJkFHc6Cch1YsAEGFEhi6wSp6MILb0sJMQ1CX06e9tCOAjSlBwaJj4PH0eFCVau5v9Q==",
"dev": true,
"requires": {
"axios": "0.26.1",
"iterare": "1.2.1",
"tslib": "2.3.1",
"uuid": "8.3.2"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@nestjs/core": {
"version": "8.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.4.4.tgz",
"integrity": "sha512-Ef3yJPuzAttpNfehnGqIV5kHIL9SHptB5F4ERxoU7pT61H3xiYpZw6hSjx68cJO7cc6rm7/N+b4zeuJvFHtvBg==",
"dev": true,
"requires": {
"@nuxtjs/opencollective": "0.3.2",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
"object-hash": "3.0.0",
"path-to-regexp": "3.2.0",
"tslib": "2.3.1",
"uuid": "8.3.2"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true
},
"fs-extra": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
"integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inquirer": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz",
"integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.1",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.21",
"mute-stream": "0.0.8",
"ora": "^5.4.1",
"run-async": "^2.4.0",
"rxjs": "^7.5.5",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
}
},
"rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
}
}
},
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
"dev": true
}
}
},
"@sideway/address": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz",
@ -13697,9 +14194,9 @@
}
},
"axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
"follow-redirects": "^1.14.8"
}
@ -14259,6 +14756,12 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"compare-versions": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz",
"integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==",
"dev": true
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -14309,6 +14812,48 @@
}
}
},
"concurrently": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
"integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"rxjs": "^6.6.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^16.2.0"
},
"dependencies": {
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
}
},
"consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@ -14319,6 +14864,15 @@
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"console.table": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz",
"integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==",
"dev": true,
"requires": {
"easy-table": "1.1.0"
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -14683,6 +15237,15 @@
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"easy-table": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
"integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==",
"dev": true,
"requires": {
"wcwidth": ">=1.0.1"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@ -18655,6 +19218,12 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@ -18858,6 +19427,19 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
"swagger-ui-dist": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.12.0.tgz",
"integrity": "sha512-B0Iy2ueXtbByE6OOyHTi3lFQkpPi/L7kFOKFeKTr44za7dJIELa9kzaca6GkndCgpK1QTjArnoXG+aUy0XQp1w=="
},
"swagger-ui-express": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.4.0.tgz",
"integrity": "sha512-1CzRkHG386VQMVZK406jcpgnW2a9A5A/NiAjKhsFTQqUBWRF+uGbXTU/mA7WSV3mTzyOQDvjBdWP/c2qd5lqKw==",
"requires": {
"swagger-ui-dist": ">=4.11.0"
}
},
"symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View file

@ -22,7 +22,8 @@
"test:cov": "jest --coverage",
"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"
"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"
},
"dependencies": {
"@mapbox/mapbox-sdk": "^0.13.3",
@ -37,6 +38,7 @@
"@nestjs/platform-fastify": "^8.4.7",
"@nestjs/platform-socket.io": "^8.4.7",
"@nestjs/schedule": "^2.0.1",
"@nestjs/swagger": "^5.2.1",
"@nestjs/typeorm": "^8.1.4",
"@nestjs/websockets": "^8.4.7",
"@socket.io/redis-adapter": "^7.1.0",
@ -60,6 +62,7 @@
"rxjs": "^7.2.0",
"sharp": "^0.28.0",
"socket.io-redis": "^6.1.1",
"swagger-ui-express": "^4.4.0",
"systeminformation": "^5.11.0",
"typeorm": "^0.3.6"
},
@ -83,6 +86,7 @@
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@openapitools/openapi-generator-cli": "2.5.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",

View file

@ -6,15 +6,15 @@ module.exports = {
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
'svelte3/typescript': () => require('typescript'),
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
ecmaVersion: 2020,
},
env: {
browser: true,
es2017: true,
node: true
}
node: true,
},
};

View file

@ -0,0 +1,34 @@
import {
AlbumApi,
AssetApi,
AuthenticationApi,
Configuration,
DeviceInfoApi,
ServerInfoApi,
UserApi,
} from '../open-api';
class ImmichApi {
public userApi: UserApi;
public albumApi: AlbumApi;
public assetApi: AssetApi;
public authenticationApi: AuthenticationApi;
public deviceInfoApi: DeviceInfoApi;
public serverInfoApi: ServerInfoApi;
private config = new Configuration();
constructor() {
this.userApi = new UserApi(this.config);
this.albumApi = new AlbumApi(this.config);
this.assetApi = new AssetApi(this.config);
this.authenticationApi = new AuthenticationApi(this.config);
this.deviceInfoApi = new DeviceInfoApi(this.config);
this.serverInfoApi = new ServerInfoApi(this.config);
}
public setAccessToken(accessToken: string) {
this.config.accessToken = accessToken;
}
}
export const immichApi = new ImmichApi();

4
web/src/lib/open-api/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist

View file

@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View file

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View file

@ -0,0 +1,9 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

View file

@ -0,0 +1 @@
6.0.1

3874
web/src/lib/open-api/api.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
/* tslint:disable */
/* eslint-disable */
/**
* Immich
* 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';
// Some imports not used depending on template conditions
// @ts-ignore
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
export const BASE_PATH = '/api'.replace(/\/+$/, '');
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ',',
ssv: ' ',
tsv: '\t',
pipes: '|',
};
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
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;
}
}
}
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
name: 'RequiredError' = 'RequiredError';
constructor(public field: string, msg?: string) {
super(msg);
}
}

View file

@ -0,0 +1,138 @@
/* tslint:disable */
/* eslint-disable */
/**
* Immich
* 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 { RequiredError, RequestArgs } from "./base";
import { AxiosInstance, AxiosResponse } from 'axios';
/**
*
* @export
*/
export const DUMMY_BASE_URL = 'https://example.com'
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password };
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken;
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search);
for (const object of objects) {
for (const key in object) {
if (Array.isArray(object[key])) {
searchParams.delete(key);
for (const item of object[key]) {
searchParams.append(key, item);
}
} else {
searchParams.set(key, object[key]);
}
}
}
url.search = searchParams.toString();
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: (value || "");
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View file

@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Immich
* 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.
*/
export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
baseOptions?: any;
formDataCtor?: new () => any;
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
}

View file

@ -0,0 +1,57 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="github.com"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="GIT_USER_ID"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="GIT_REPO_ID"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="Minor update"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View file

@ -0,0 +1,18 @@
/* tslint:disable */
/* eslint-disable */
/**
* Immich
* 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.
*/
export * from "./api";
export * from "./configuration";

View file

@ -3,7 +3,6 @@
import type { Load } from '@sveltejs/kit';
import { getAssetsInfo } from '$lib/stores/assets';
import { checkAppVersion } from '$lib/utils/check-app-version';
export const load: Load = async ({ session }) => {
if (!session.user) {
@ -25,7 +24,7 @@
<script lang="ts">
import type { ImmichUser } from '$lib/models/immich-user';
import NavigationBar from '../../lib/components/shared/navigation-bar.svelte';
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
import SideBarButton from '$lib/components/shared/side-bar-button.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
@ -35,16 +34,16 @@
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 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 DownloadPanel from '../../lib/components/asset-viewer/download-panel.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 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';
export let user: ImmichUser;
let selectedAction: AppSideBarSelection;
let selectedGroupThumbnail: number | null;

View file

@ -16,5 +16,7 @@
"sourceMap": true,
"strict": true,
"target": "es2020",
}
"importsNotUsedAsValues": "preserve",
"preserveValueImports": false
},
}