mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Set TypeScript to strict mode and fix issues related to server types (#261)
* Fix lint issues and some other TS issues - set TypeScript in strict mode - add npm commands to lint / check code - fix all lint issues - fix some TS issues - rename User reponse DTO to make it consistent with the other ones - override Express/User interface to use UserResponseDto interface This is for when the accessing the `user` from a Express Request, like in `asset-upload-config` * Fix the rest of TS issues - fix all the remaining TypeScript errors - add missing `@types/mapbox__mapbox-sdk` package * Move global.d.ts to server `src` folder * Update AssetReponseDto duration type This is now of type `string` that defaults to '0:00:00.00000' if not set which is what the mobile app currently expects * Set context when logging error in asset.service Use `ServeFile` as the context for logging an error when asset.resizePath is not set * Fix wrong AppController merge conflict resolution `redirectToWebpage` was removed in main as is no longer used.
This commit is contained in:
		| @@ -14,7 +14,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto'; | |||||||
| export interface IAlbumRepository { | export interface IAlbumRepository { | ||||||
|   create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>; |   create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>; | ||||||
|   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>; |   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>; | ||||||
|   get(albumId: string): Promise<AlbumEntity>; |   get(albumId: string): Promise<AlbumEntity | undefined>; | ||||||
|   delete(album: AlbumEntity): Promise<void>; |   delete(album: AlbumEntity): Promise<void>; | ||||||
|   addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>; |   addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>; | ||||||
|   removeUser(album: AlbumEntity, userId: string): Promise<void>; |   removeUser(album: AlbumEntity, userId: string): Promise<void>; | ||||||
| @@ -39,7 +39,7 @@ export class AlbumRepository implements IAlbumRepository { | |||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> { |   async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> { | ||||||
|     return await getConnection().transaction(async (transactionalEntityManager) => { |     return getConnection().transaction(async (transactionalEntityManager) => { | ||||||
|       // Create album entity |       // Create album entity | ||||||
|       const newAlbum = new AlbumEntity(); |       const newAlbum = new AlbumEntity(); | ||||||
|       newAlbum.ownerId = ownerId; |       newAlbum.ownerId = ownerId; | ||||||
| @@ -80,7 +80,6 @@ export class AlbumRepository implements IAlbumRepository { | |||||||
|  |  | ||||||
|       return album; |       return album; | ||||||
|     }); |     }); | ||||||
|     return; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> { |   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> { | ||||||
| @@ -155,7 +154,7 @@ export class AlbumRepository implements IAlbumRepository { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     // TODO: sort in query |     // TODO: sort in query | ||||||
|     const sortedSharedAsset = album.assets.sort( |     const sortedSharedAsset = album.assets?.sort( | ||||||
|       (a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(), |       (a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -180,7 +179,7 @@ export class AlbumRepository implements IAlbumRepository { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     await this.userAlbumRepository.save([...newRecords]); |     await this.userAlbumRepository.save([...newRecords]); | ||||||
|     return this.get(album.id); |     return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async removeUser(album: AlbumEntity, userId: string): Promise<void> { |   async removeUser(album: AlbumEntity, userId: string): Promise<void> { | ||||||
| @@ -217,7 +216,7 @@ export class AlbumRepository implements IAlbumRepository { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     await this.assetAlbumRepository.save([...newRecords]); |     await this.assetAlbumRepository.save([...newRecords]); | ||||||
|     return this.get(album.id); |     return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> { |   updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> { | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ describe('Album service', () => { | |||||||
|     albumEntity.createdAt = 'date'; |     albumEntity.createdAt = 'date'; | ||||||
|     albumEntity.sharedUsers = []; |     albumEntity.sharedUsers = []; | ||||||
|     albumEntity.assets = []; |     albumEntity.assets = []; | ||||||
|  |     albumEntity.albumThumbnailAssetId = null; | ||||||
|  |  | ||||||
|     return albumEntity; |     return albumEntity; | ||||||
|   }; |   }; | ||||||
| @@ -36,6 +37,7 @@ describe('Album service', () => { | |||||||
|     albumEntity.albumName = 'name'; |     albumEntity.albumName = 'name'; | ||||||
|     albumEntity.createdAt = 'date'; |     albumEntity.createdAt = 'date'; | ||||||
|     albumEntity.assets = []; |     albumEntity.assets = []; | ||||||
|  |     albumEntity.albumThumbnailAssetId = null; | ||||||
|     albumEntity.sharedUsers = [ |     albumEntity.sharedUsers = [ | ||||||
|       { |       { | ||||||
|         id: '99', |         id: '99', | ||||||
| @@ -60,6 +62,7 @@ describe('Album service', () => { | |||||||
|     albumEntity.albumName = 'name'; |     albumEntity.albumName = 'name'; | ||||||
|     albumEntity.createdAt = 'date'; |     albumEntity.createdAt = 'date'; | ||||||
|     albumEntity.assets = []; |     albumEntity.assets = []; | ||||||
|  |     albumEntity.albumThumbnailAssetId = null; | ||||||
|     albumEntity.sharedUsers = [ |     albumEntity.sharedUsers = [ | ||||||
|       { |       { | ||||||
|         id: '99', |         id: '99', | ||||||
| @@ -96,6 +99,7 @@ describe('Album service', () => { | |||||||
|     albumEntity.createdAt = 'date'; |     albumEntity.createdAt = 'date'; | ||||||
|     albumEntity.sharedUsers = []; |     albumEntity.sharedUsers = []; | ||||||
|     albumEntity.assets = []; |     albumEntity.assets = []; | ||||||
|  |     albumEntity.albumThumbnailAssetId = null; | ||||||
|  |  | ||||||
|     return albumEntity; |     return albumEntity; | ||||||
|   }; |   }; | ||||||
| @@ -151,7 +155,7 @@ describe('Album service', () => { | |||||||
|  |  | ||||||
|     const expectedResult: AlbumResponseDto = { |     const expectedResult: AlbumResponseDto = { | ||||||
|       albumName: 'name', |       albumName: 'name', | ||||||
|       albumThumbnailAssetId: undefined, |       albumThumbnailAssetId: null, | ||||||
|       createdAt: 'date', |       createdAt: 'date', | ||||||
|       id: '0001', |       id: '0001', | ||||||
|       ownerId, |       ownerId, | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ export class AlbumService { | |||||||
|  |  | ||||||
|     if (validateIsOwner && !isOwner) { |     if (validateIsOwner && !isOwner) { | ||||||
|       throw new ForbiddenException('Unauthorized Album Access'); |       throw new ForbiddenException('Unauthorized Album Access'); | ||||||
|     } else if (!isOwner && !album.sharedUsers.some((user) => user.sharedUserId == authUser.id)) { |     } else if (!isOwner && !album.sharedUsers?.some((user) => user.sharedUserId == authUser.id)) { | ||||||
|       throw new ForbiddenException('Unauthorized Album Access'); |       throw new ForbiddenException('Unauthorized Album Access'); | ||||||
|     } |     } | ||||||
|     return album; |     return album; | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class AddAssetsDto { | export class AddAssetsDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   assetIds: string[]; |   assetIds!: string[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class AddUsersDto { | export class AddUsersDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   sharedUserIds: string[]; |   sharedUserIds!: string[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; | |||||||
|  |  | ||||||
| export class CreateAlbumDto { | export class CreateAlbumDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   albumName: string; |   albumName!: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   sharedWithUserIds?: string[]; |   sharedWithUserIds?: string[]; | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class RemoveAssetsDto { | export class RemoveAssetsDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   assetIds: string[]; |   assetIds!: string[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class UpdateAlbumDto { | export class UpdateAlbumDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   albumName: string; |   albumName!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   ownerId: string; |   ownerId!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity'; | import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity'; | ||||||
| import { User, mapUser } from '../../user/response-dto/user'; | import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; | ||||||
| import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; | import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; | ||||||
|  |  | ||||||
| export interface AlbumResponseDto { | export interface AlbumResponseDto { | ||||||
| @@ -9,7 +9,7 @@ export interface AlbumResponseDto { | |||||||
|   createdAt: string; |   createdAt: string; | ||||||
|   albumThumbnailAssetId: string | null; |   albumThumbnailAssetId: string | null; | ||||||
|   shared: boolean; |   shared: boolean; | ||||||
|   sharedUsers: User[]; |   sharedUsers: UserResponseDto[]; | ||||||
|   assets: AssetResponseDto[]; |   assets: AssetResponseDto[]; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import { | |||||||
|   Headers, |   Headers, | ||||||
|   Delete, |   Delete, | ||||||
|   Logger, |   Logger, | ||||||
|   Patch, |  | ||||||
|   HttpCode, |   HttpCode, | ||||||
| } 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'; | ||||||
| @@ -25,9 +24,7 @@ import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | |||||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | 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 { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; |  | ||||||
| import { Response as Res } from 'express'; | import { Response as Res } from 'express'; | ||||||
| import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; |  | ||||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | import { SearchAssetDto } from './dto/search-asset.dto'; | ||||||
| @@ -58,15 +55,18 @@ export class AssetController { | |||||||
|     ), |     ), | ||||||
|   ) |   ) | ||||||
|   async uploadFile( |   async uploadFile( | ||||||
|     @GetAuthUser() authUser, |     @GetAuthUser() authUser: AuthUserDto, | ||||||
|     @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] }, |     @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] }, | ||||||
|     @Body(ValidationPipe) assetInfo: CreateAssetDto, |     @Body(ValidationPipe) assetInfo: CreateAssetDto, | ||||||
|   ) { |   ): Promise<'ok' | undefined> { | ||||||
|     for (const file of uploadFiles.assetData) { |     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 (uploadFiles.thumbnailData != null && savedAsset) { |         if (!savedAsset) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (uploadFiles.thumbnailData != null) { | ||||||
|           const assetWithThumbnail = await this.assetService.updateThumbnailInfo( |           const assetWithThumbnail = await this.assetService.updateThumbnailInfo( | ||||||
|             savedAsset, |             savedAsset, | ||||||
|             uploadFiles.thumbnailData[0].path, |             uploadFiles.thumbnailData[0].path, | ||||||
| @@ -107,11 +107,11 @@ export class AssetController { | |||||||
|  |  | ||||||
|   @Get('/file') |   @Get('/file') | ||||||
|   async serveFile( |   async serveFile( | ||||||
|     @Headers() headers, |     @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(ValidationPipe) query: ServeFileDto, | ||||||
|   ): Promise<StreamableFile> { |   ): Promise<StreamableFile | undefined> { | ||||||
|     return this.assetService.serveFile(authUser, query, res, headers); |     return this.assetService.serveFile(authUser, query, res, headers); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -151,7 +151,7 @@ export class AssetController { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Get('/assetById/:assetId') |   @Get('/assetById/:assetId') | ||||||
|   async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) { |   async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId: string) { | ||||||
|     return await this.assetService.getAssetById(authUser, assetId); |     return await this.assetService.getAssetById(authUser, assetId); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -161,6 +161,9 @@ export class AssetController { | |||||||
|  |  | ||||||
|     for (const id of assetIds.ids) { |     for (const id of assetIds.ids) { | ||||||
|       const assets = await this.assetService.getAssetById(authUser, id); |       const assets = await this.assetService.getAssetById(authUser, id); | ||||||
|  |       if (!assets) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|       deleteAssetList.push(assets); |       deleteAssetList.push(assets); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,16 @@ | |||||||
| import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common'; | import { | ||||||
|  |   BadRequestException, | ||||||
|  |   Injectable, | ||||||
|  |   InternalServerErrorException, | ||||||
|  |   Logger, | ||||||
|  |   NotFoundException, | ||||||
|  |   StreamableFile, | ||||||
|  | } from '@nestjs/common'; | ||||||
| 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 { 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 _ from 'lodash'; |  | ||||||
| import { createReadStream, stat } from 'fs'; | import { createReadStream, stat } from 'fs'; | ||||||
| import { ServeFileDto } from './dto/serve-file.dto'; | import { ServeFileDto } from './dto/serve-file.dto'; | ||||||
| import { Response as Res } from 'express'; | import { Response as Res } from 'express'; | ||||||
| @@ -33,7 +39,12 @@ export class AssetService { | |||||||
|     return updatedAsset.raw[0]; |     return updatedAsset.raw[0]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) { |   public async createUserAsset( | ||||||
|  |     authUser: AuthUserDto, | ||||||
|  |     assetInfo: CreateAssetDto, | ||||||
|  |     path: string, | ||||||
|  |     mimeType: string, | ||||||
|  |   ): Promise<AssetEntity | undefined> { | ||||||
|     const asset = new AssetEntity(); |     const asset = new AssetEntity(); | ||||||
|     asset.deviceAssetId = assetInfo.deviceAssetId; |     asset.deviceAssetId = assetInfo.deviceAssetId; | ||||||
|     asset.userId = authUser.id; |     asset.userId = authUser.id; | ||||||
| @@ -44,10 +55,14 @@ export class AssetService { | |||||||
|     asset.modifiedAt = assetInfo.modifiedAt; |     asset.modifiedAt = assetInfo.modifiedAt; | ||||||
|     asset.isFavorite = assetInfo.isFavorite; |     asset.isFavorite = assetInfo.isFavorite; | ||||||
|     asset.mimeType = mimeType; |     asset.mimeType = mimeType; | ||||||
|     asset.duration = assetInfo.duration; |     asset.duration = assetInfo.duration || null; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       return await this.assetRepository.save(asset); |       const createdAsset = await this.assetRepository.save(asset); | ||||||
|  |       if (!createdAsset) { | ||||||
|  |         throw new Error('Asset not created'); | ||||||
|  |       } | ||||||
|  |       return createdAsset; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); |       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); | ||||||
|     } |     } | ||||||
| @@ -62,7 +77,7 @@ export class AssetService { | |||||||
|       select: ['deviceAssetId'], |       select: ['deviceAssetId'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const res = []; |     const res: string[] = []; | ||||||
|     rows.forEach((v) => res.push(v.deviceAssetId)); |     rows.forEach((v) => res.push(v.deviceAssetId)); | ||||||
|     return res; |     return res; | ||||||
|   } |   } | ||||||
| @@ -119,6 +134,9 @@ export class AssetService { | |||||||
|         }); |         }); | ||||||
|         file = createReadStream(asset.originalPath); |         file = createReadStream(asset.originalPath); | ||||||
|       } else { |       } else { | ||||||
|  |         if (!asset.resizePath) { | ||||||
|  |           throw new Error('resizePath not set'); | ||||||
|  |         } | ||||||
|         const { size } = await fileInfo(asset.resizePath); |         const { size } = await fileInfo(asset.resizePath); | ||||||
|         res.set({ |         res.set({ | ||||||
|           'Content-Type': 'image/jpeg', |           'Content-Type': 'image/jpeg', | ||||||
| @@ -134,16 +152,25 @@ export class AssetService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async getAssetThumbnail(assetId: string) { |   public async getAssetThumbnail(assetId: string): Promise<StreamableFile> { | ||||||
|     try { |     try { | ||||||
|       const asset = await this.assetRepository.findOne({ id: assetId }); |       const asset = await this.assetRepository.findOne({ id: assetId }); | ||||||
|  |       if (!asset) { | ||||||
|  |         throw new NotFoundException('Asset not found'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (asset.webpPath && asset.webpPath.length > 0) { |       if (asset.webpPath && asset.webpPath.length > 0) { | ||||||
|         return new StreamableFile(createReadStream(asset.webpPath)); |         return new StreamableFile(createReadStream(asset.webpPath)); | ||||||
|       } else { |       } else { | ||||||
|  |         if (!asset.resizePath) { | ||||||
|  |           throw new Error('resizePath not set'); | ||||||
|  |         } | ||||||
|         return new StreamableFile(createReadStream(asset.resizePath)); |         return new StreamableFile(createReadStream(asset.resizePath)); | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  |       if (e instanceof NotFoundException) { | ||||||
|  |         throw e; | ||||||
|  |       } | ||||||
|       Logger.error('Error serving asset thumbnail ', e); |       Logger.error('Error serving asset thumbnail ', e); | ||||||
|       throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail'); |       throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail'); | ||||||
|     } |     } | ||||||
| @@ -154,6 +181,7 @@ export class AssetService { | |||||||
|     const asset = await this.findOne(query.did, query.aid); |     const asset = await this.findOne(query.did, query.aid); | ||||||
|  |  | ||||||
|     if (!asset) { |     if (!asset) { | ||||||
|  |       // TODO: maybe this should be a NotFoundException? | ||||||
|       throw new BadRequestException('Asset does not exist'); |       throw new BadRequestException('Asset does not exist'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -166,6 +194,10 @@ export class AssetService { | |||||||
|         res.set({ |         res.set({ | ||||||
|           'Content-Type': 'image/jpeg', |           'Content-Type': 'image/jpeg', | ||||||
|         }); |         }); | ||||||
|  |         if (!asset.resizePath) { | ||||||
|  |           Logger.error('Error serving IMAGE asset for web', 'ServeFile'); | ||||||
|  |           throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); | ||||||
|  |         } | ||||||
|         return new StreamableFile(createReadStream(asset.resizePath)); |         return new StreamableFile(createReadStream(asset.resizePath)); | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -189,6 +221,9 @@ export class AssetService { | |||||||
|             res.set({ |             res.set({ | ||||||
|               'Content-Type': 'image/jpeg', |               'Content-Type': 'image/jpeg', | ||||||
|             }); |             }); | ||||||
|  |             if (!asset.resizePath) { | ||||||
|  |               throw new Error('resizePath not set'); | ||||||
|  |             } | ||||||
|             file = createReadStream(asset.resizePath); |             file = createReadStream(asset.resizePath); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -297,6 +332,7 @@ export class AssetService { | |||||||
|  |  | ||||||
|   async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> { |   async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> { | ||||||
|     const possibleSearchTerm = new Set<string>(); |     const possibleSearchTerm = new Set<string>(); | ||||||
|  |     // TODO: should use query builder | ||||||
|     const rows = await this.assetRepository.query( |     const rows = await this.assetRepository.query( | ||||||
|       ` |       ` | ||||||
|       select distinct si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country |       select distinct si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country | ||||||
| @@ -308,12 +344,12 @@ export class AssetService { | |||||||
|       [authUser.id], |       [authUser.id], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     rows.forEach((row) => { |     rows.forEach((row: { [x: string]: any }) => { | ||||||
|       // tags |       // tags | ||||||
|       row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); |       row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); | ||||||
|  |  | ||||||
|       // objects |       // objects | ||||||
|       row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase())); |       row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase())); | ||||||
|  |  | ||||||
|       // asset's tyoe |       // asset's tyoe | ||||||
|       possibleSearchTerm.add(row['type']?.toLowerCase()); |       possibleSearchTerm.add(row['type']?.toLowerCase()); | ||||||
| @@ -345,7 +381,7 @@ export class AssetService { | |||||||
|              LEFT JOIN exif e ON a.id = e."assetId" |              LEFT JOIN exif e ON a.id = e."assetId" | ||||||
|  |  | ||||||
|     WHERE a."userId" = $1 |     WHERE a."userId" = $1 | ||||||
|        AND  |        AND | ||||||
|        ( |        ( | ||||||
|          TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR |          TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR | ||||||
|          TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR |          TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR | ||||||
| @@ -362,7 +398,7 @@ export class AssetService { | |||||||
|         select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" |         select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" | ||||||
|         from assets a |         from assets a | ||||||
|         left join exif e on a.id = e."assetId" |         left join exif e on a.id = e."assetId" | ||||||
|         where a."userId" = $1  |         where a."userId" = $1 | ||||||
|         and e.city is not null |         and e.city is not null | ||||||
|         and a.type = 'IMAGE'; |         and a.type = 'IMAGE'; | ||||||
|       `, |       `, | ||||||
| @@ -376,7 +412,7 @@ export class AssetService { | |||||||
|         select distinct on (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" |         select distinct on (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" | ||||||
|         from assets a |         from assets a | ||||||
|         left join smart_info si on a.id = si."assetId" |         left join smart_info si on a.id = si."assetId" | ||||||
|         where a."userId" = $1  |         where a."userId" = $1 | ||||||
|         and si.objects is not null |         and si.objects is not null | ||||||
|       `, |       `, | ||||||
|       [authUser.id], |       [authUser.id], | ||||||
|   | |||||||
| @@ -3,26 +3,26 @@ import { AssetType } from '@app/database/entities/asset.entity'; | |||||||
|  |  | ||||||
| export class CreateAssetDto { | export class CreateAssetDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   deviceAssetId: string; |   deviceAssetId!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   deviceId: string; |   deviceId!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   assetType: AssetType; |   assetType!: AssetType; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   createdAt: string; |   createdAt!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   modifiedAt: string; |   modifiedAt!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   isFavorite: boolean; |   isFavorite!: boolean; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   fileExtension: string; |   fileExtension!: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   duration: string; |   duration?: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,47 +2,47 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; | |||||||
|  |  | ||||||
| export class CreateExifDto { | export class CreateExifDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   assetId: string; |   assetId!: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   make: string; |   make?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   model: string; |   model?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   imageName: string; |   imageName?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   exifImageWidth: number; |   exifImageWidth?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   exifImageHeight: number; |   exifImageHeight?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   fileSizeInByte: number; |   fileSizeInByte?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   orientation: string; |   orientation?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   dateTimeOriginal: Date; |   dateTimeOriginal?: Date; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   modifiedDate: Date; |   modifiedDate?: Date; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   lensModel: string; |   lensModel?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   fNumber: number; |   fNumber?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   focalLenght: number; |   focalLenght?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   iso: number; |   iso?: number; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   exposureTime: number; |   exposureTime?: number; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class DeleteAssetDto { | export class DeleteAssetDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   ids: string[]; |   ids!: string[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | import { IsOptional } from 'class-validator'; | ||||||
|  |  | ||||||
| export class GetAllAssetQueryDto { | export class GetAllAssetQueryDto { | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   nextPageKey: string; |   nextPageKey?: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||||
|  |  | ||||||
|  | // TODO: this doesn't seem to be used | ||||||
| export class GetAllAssetReponseDto { | export class GetAllAssetReponseDto { | ||||||
|   data: Array<{ date: string; assets: Array<AssetEntity> }>; |   data!: Array<{ date: string; assets: Array<AssetEntity> }>; | ||||||
|   count: number; |   count!: number; | ||||||
|   nextPageKey: string; |   nextPageKey!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class GetAssetDto { | export class GetAssetDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   deviceId: string; |   deviceId!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class GetNewAssetQueryDto { | export class GetNewAssetQueryDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   latestDate: string; |   latestDate!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class SearchAssetDto { | export class SearchAssetDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   searchTerm: string; |   searchTerm!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,20 +1,19 @@ | |||||||
| import { Transform } from 'class-transformer'; | import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator'; | ||||||
| import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator'; |  | ||||||
|  |  | ||||||
| export class ServeFileDto { | export class ServeFileDto { | ||||||
|   //assetId |   //assetId | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   aid: string; |   aid!: string; | ||||||
|  |  | ||||||
|   //deviceId |   //deviceId | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   did: string; |   did!: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   @IsBooleanString() |   @IsBooleanString() | ||||||
|   isThumb: string; |   isThumb?: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   @IsBooleanString() |   @IsBooleanString() | ||||||
|   isWeb: string; |   isWeb?: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ export interface AssetResponseDto { | |||||||
|   modifiedAt: string; |   modifiedAt: string; | ||||||
|   isFavorite: boolean; |   isFavorite: boolean; | ||||||
|   mimeType: string | null; |   mimeType: string | null; | ||||||
|   duration: string | null; |   duration: string; | ||||||
|   exifInfo?: ExifResponseDto; |   exifInfo?: ExifResponseDto; | ||||||
|   smartInfo?: SmartInfoResponseDto; |   smartInfo?: SmartInfoResponseDto; | ||||||
| } | } | ||||||
| @@ -32,7 +32,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { | |||||||
|     modifiedAt: entity.modifiedAt, |     modifiedAt: entity.modifiedAt, | ||||||
|     isFavorite: entity.isFavorite, |     isFavorite: entity.isFavorite, | ||||||
|     mimeType: entity.mimeType, |     mimeType: entity.mimeType, | ||||||
|     duration: entity.duration, |     duration: entity.duration ?? '0:00:00.00000', | ||||||
|     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, |     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, | ||||||
|     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, |     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ export class AuthController { | |||||||
|  |  | ||||||
|   @UseGuards(JwtAuthGuard) |   @UseGuards(JwtAuthGuard) | ||||||
|   @Post('/validateToken') |   @Post('/validateToken') | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||
|   async validateToken(@GetAuthUser() authUser: AuthUserDto) { |   async validateToken(@GetAuthUser() authUser: AuthUserDto) { | ||||||
|     return { |     return { | ||||||
|       authStatus: true, |       authStatus: true, | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | |||||||
| import { JwtPayloadDto } from './dto/jwt-payload.dto'; | import { JwtPayloadDto } from './dto/jwt-payload.dto'; | ||||||
| import { SignUpDto } from './dto/sign-up.dto'; | import { SignUpDto } from './dto/sign-up.dto'; | ||||||
| import * as bcrypt from 'bcrypt'; | import * as bcrypt from 'bcrypt'; | ||||||
| import { mapUser, User } from '../user/response-dto/user'; | import { mapUser, UserResponseDto } from '../user/response-dto/user-response.dto'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class AuthService { | export class AuthService { | ||||||
| @@ -39,7 +39,8 @@ export class AuthService { | |||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt); |     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||||
|  |     const isAuthenticated = await this.validatePassword(user.password!, loginCredential.password, user.salt!); | ||||||
|  |  | ||||||
|     if (isAuthenticated) { |     if (isAuthenticated) { | ||||||
|       return user; |       return user; | ||||||
| @@ -69,7 +70,7 @@ export class AuthService { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async adminSignUp(signUpCredential: SignUpDto): Promise<User> { |   public async adminSignUp(signUpCredential: SignUpDto): Promise<UserResponseDto> { | ||||||
|     const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } }); |     const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } }); | ||||||
|  |  | ||||||
|     if (adminUser) { |     if (adminUser) { | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class LoginCredentialDto { | export class LoginCredentialDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   email: string; |   email!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   password: string; |   password!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,14 +2,14 @@ import { IsNotEmpty } from 'class-validator'; | |||||||
|  |  | ||||||
| export class SignUpDto { | export class SignUpDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   email: string; |   email!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   password: string; |   password!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   firstName: string; |   firstName!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   lastName: string; |   lastName!: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; | import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; | ||||||
| import { CommunicationService } from './communication.service'; |  | ||||||
| import { Socket, Server } from 'socket.io'; | import { Socket, Server } from 'socket.io'; | ||||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | import { ImmichJwtService, JwtValidationResult } from '../../modules/immich-jwt/immich-jwt.service'; | ||||||
| import { Logger } from '@nestjs/common'; | import { Logger } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { UserEntity } from '@app/database/entities/user.entity'; | import { UserEntity } from '@app/database/entities/user.entity'; | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
| import { query } from 'express'; |  | ||||||
|  |  | ||||||
| @WebSocketGateway({ cors: true }) | @WebSocketGateway({ cors: true }) | ||||||
| export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect { | export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect { | ||||||
| @@ -17,7 +15,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco | |||||||
|     private userRepository: Repository<UserEntity>, |     private userRepository: Repository<UserEntity>, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @WebSocketServer() server: Server; |   @WebSocketServer() server!: Server; | ||||||
|  |  | ||||||
|   handleDisconnect(client: Socket) { |   handleDisconnect(client: Socket) { | ||||||
|     client.leave(client.nsp.name); |     client.leave(client.nsp.name); | ||||||
| @@ -25,13 +23,15 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco | |||||||
|     Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent'); |     Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleConnection(client: Socket, ...args: any[]) { |   async handleConnection(client: Socket) { | ||||||
|     try { |     try { | ||||||
|       Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent'); |       Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent'); | ||||||
|  |  | ||||||
|       const accessToken = client.handshake.headers.authorization.split(' ')[1]; |       const accessToken = client.handshake.headers.authorization?.split(' ')[1]; | ||||||
|  |  | ||||||
|       const res = await this.immichJwtService.validateToken(accessToken); |       const res: JwtValidationResult = accessToken | ||||||
|  |         ? await this.immichJwtService.validateToken(accessToken) | ||||||
|  |         : { status: false, userId: null }; | ||||||
|  |  | ||||||
|       if (!res.status) { |       if (!res.status) { | ||||||
|         client.emit('error', 'unauthorized'); |         client.emit('error', 'unauthorized'); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe } from '@nestjs/common'; | import { Controller, Post, Body, Patch, UseGuards, ValidationPipe } from '@nestjs/common'; | ||||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||||
| import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | ||||||
| import { DeviceInfoService } from './device-info.service'; | import { DeviceInfoService } from './device-info.service'; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { BadRequestException, HttpCode, Injectable, Logger, Res } from '@nestjs/common'; | import { BadRequestException, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||||
|   | |||||||
| @@ -3,11 +3,11 @@ import { DeviceType } from '@app/database/entities/device-info.entity'; | |||||||
|  |  | ||||||
| export class CreateDeviceInfoDto { | export class CreateDeviceInfoDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   deviceId: string; |   deviceId!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   deviceType: DeviceType; |   deviceType!: DeviceType; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   isAutoBackup: boolean; |   isAutoBackup?: boolean; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| import { PartialType } from '@nestjs/mapped-types'; | import { PartialType } from '@nestjs/mapped-types'; | ||||||
| import { IsOptional } from 'class-validator'; |  | ||||||
| import { DeviceType } from '@app/database/entities/device-info.entity'; |  | ||||||
| import { CreateDeviceInfoDto } from './create-device-info.dto'; | import { CreateDeviceInfoDto } from './create-device-info.dto'; | ||||||
|  |  | ||||||
| export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {} | export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {} | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
|  | // TODO: this is being used as a response DTO. Should be changed to interface | ||||||
| export class ServerInfoDto { | export class ServerInfoDto { | ||||||
|   diskSize: string; |   diskSize!: string; | ||||||
|   diskUse: string; |   diskUse!: string; | ||||||
|   diskAvailable: string; |   diskAvailable!: string; | ||||||
|   diskSizeRaw: number; |   diskSizeRaw!: number; | ||||||
|   diskUseRaw: number; |   diskUseRaw!: number; | ||||||
|   diskAvailableRaw: number; |   diskAvailableRaw!: number; | ||||||
|   diskUsagePercentage: number; |   diskUsagePercentage!: number; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,16 +2,16 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; | |||||||
|  |  | ||||||
| export class CreateUserDto { | export class CreateUserDto { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   email: string; |   email!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   password: string; |   password!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   firstName: string; |   firstName!: string; | ||||||
|  |  | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   lastName: string; |   lastName!: string; | ||||||
|  |  | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   profileImagePath?: string; |   profileImagePath?: string; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity'; | import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity'; | ||||||
| 
 | 
 | ||||||
| export interface User { | export interface UserResponseDto { | ||||||
|   id: string; |   id: string; | ||||||
|   email: string; |   email: string; | ||||||
|   firstName: string; |   firstName: string; | ||||||
| @@ -8,7 +8,7 @@ export interface User { | |||||||
|   createdAt: string; |   createdAt: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapUser(entity: UserEntity): User { | export function mapUser(entity: UserEntity): UserResponseDto { | ||||||
|   return { |   return { | ||||||
|     id: entity.id, |     id: entity.id, | ||||||
|     email: entity.email, |     email: entity.email, | ||||||
| @@ -3,9 +3,7 @@ import { | |||||||
|   Get, |   Get, | ||||||
|   Post, |   Post, | ||||||
|   Body, |   Body, | ||||||
|   Patch, |  | ||||||
|   Param, |   Param, | ||||||
|   Delete, |  | ||||||
|   UseGuards, |   UseGuards, | ||||||
|   ValidationPipe, |   ValidationPipe, | ||||||
|   Put, |   Put, | ||||||
|   | |||||||
| @@ -1,4 +1,11 @@ | |||||||
| import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common'; | import { | ||||||
|  |   BadRequestException, | ||||||
|  |   Injectable, | ||||||
|  |   InternalServerErrorException, | ||||||
|  |   Logger, | ||||||
|  |   NotFoundException, | ||||||
|  |   StreamableFile, | ||||||
|  | } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Not, Repository } from 'typeorm'; | import { Not, Repository } from 'typeorm'; | ||||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||||
| @@ -8,7 +15,7 @@ import { UserEntity } from '@app/database/entities/user.entity'; | |||||||
| import * as bcrypt from 'bcrypt'; | import * as bcrypt from 'bcrypt'; | ||||||
| import { createReadStream } from 'fs'; | import { createReadStream } from 'fs'; | ||||||
| import { Response as Res } from 'express'; | import { Response as Res } from 'express'; | ||||||
| import { mapUser, User } from './response-dto/user'; | import { mapUser, UserResponseDto } from './response-dto/user-response.dto'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class UserService { | export class UserService { | ||||||
| @@ -44,7 +51,7 @@ export class UserService { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async createUser(createUserDto: CreateUserDto): Promise<User> { |   async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.findOne({ where: { email: createUserDto.email } }); |     const user = await this.userRepository.findOne({ where: { email: createUserDto.email } }); | ||||||
|  |  | ||||||
|     if (user) { |     if (user) { | ||||||
| @@ -75,6 +82,9 @@ export class UserService { | |||||||
|  |  | ||||||
|   async updateUser(updateUserDto: UpdateUserDto) { |   async updateUser(updateUserDto: UpdateUserDto) { | ||||||
|     const user = await this.userRepository.findOne(updateUserDto.id); |     const user = await this.userRepository.findOne(updateUserDto.id); | ||||||
|  |     if (!user) { | ||||||
|  |       throw new NotFoundException('User not found'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     user.lastName = updateUserDto.lastName || user.lastName; |     user.lastName = updateUserDto.lastName || user.lastName; | ||||||
|     user.firstName = updateUserDto.firstName || user.firstName; |     user.firstName = updateUserDto.firstName || user.firstName; | ||||||
| @@ -100,6 +110,7 @@ export class UserService { | |||||||
|     try { |     try { | ||||||
|       const updatedUser = await this.userRepository.save(user); |       const updatedUser = await this.userRepository.save(user); | ||||||
|  |  | ||||||
|  |       // TODO: this should probably retrun UserResponseDto | ||||||
|       return { |       return { | ||||||
|         id: updatedUser.id, |         id: updatedUser.id, | ||||||
|         email: updatedUser.email, |         email: updatedUser.email, | ||||||
| @@ -133,6 +144,9 @@ export class UserService { | |||||||
|   async getUserProfileImage(userId: string, res: Res) { |   async getUserProfileImage(userId: string, res: Res) { | ||||||
|     try { |     try { | ||||||
|       const user = await this.userRepository.findOne({ id: userId }); |       const user = await this.userRepository.findOne({ id: userId }); | ||||||
|  |       if (!user) { | ||||||
|  |         throw new NotFoundException('User not found'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (!user.profileImagePath) { |       if (!user.profileImagePath) { | ||||||
|         // throw new BadRequestException('User does not have a profile image'); |         // throw new BadRequestException('User does not have a profile image'); | ||||||
|   | |||||||
| @@ -1,6 +1,3 @@ | |||||||
| import { Controller, Get, Res, Headers } from '@nestjs/common'; | import { Controller } from '@nestjs/common'; | ||||||
| import { Response } from 'express'; |  | ||||||
| @Controller() | @Controller() | ||||||
| export class AppController { | export class AppController {} | ||||||
|   constructor() {} |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -4,8 +4,7 @@ import { AssetModule } from './api-v1/asset/asset.module'; | |||||||
| import { AuthModule } from './api-v1/auth/auth.module'; | import { AuthModule } from './api-v1/auth/auth.module'; | ||||||
| import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module'; | import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module'; | ||||||
| import { DeviceInfoModule } from './api-v1/device-info/device-info.module'; | import { DeviceInfoModule } from './api-v1/device-info/device-info.module'; | ||||||
| import { AppLoggerMiddleware } from './middlewares/app-logger.middleware'; | import { ConfigModule } from '@nestjs/config'; | ||||||
| import { ConfigModule, ConfigService } from '@nestjs/config'; |  | ||||||
| import { immichAppConfig } from './config/app.config'; | import { immichAppConfig } from './config/app.config'; | ||||||
| import { BullModule } from '@nestjs/bull'; | import { BullModule } from '@nestjs/bull'; | ||||||
| import { ServerInfoModule } from './api-v1/server-info/server-info.module'; | import { ServerInfoModule } from './api-v1/server-info/server-info.module'; | ||||||
| @@ -57,6 +56,8 @@ import { DatabaseModule } from '@app/database'; | |||||||
|   providers: [], |   providers: [], | ||||||
| }) | }) | ||||||
| export class AppModule implements NestModule { | export class AppModule implements NestModule { | ||||||
|  |   // TODO: check if consumer is needed or remove | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||
|   configure(consumer: MiddlewareConsumer): void { |   configure(consumer: MiddlewareConsumer): void { | ||||||
|     if (process.env.NODE_ENV == 'development') { |     if (process.env.NODE_ENV == 'development') { | ||||||
|       // consumer.apply(AppLoggerMiddleware).forRoutes('*'); |       // consumer.apply(AppLoggerMiddleware).forRoutes('*'); | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import { extname } from 'path'; | |||||||
| import { Request } from 'express'; | import { Request } from 'express'; | ||||||
| import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant'; | import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant'; | ||||||
| import { randomUUID } from 'crypto'; | import { randomUUID } from 'crypto'; | ||||||
| import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto'; | // import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto'; | ||||||
|  |  | ||||||
| export const assetUploadOption: MulterOptions = { | export const assetUploadOption: MulterOptions = { | ||||||
|   fileFilter: (req: Request, file: any, cb: any) => { |   fileFilter: (req: Request, file: any, cb: any) => { | ||||||
| @@ -20,13 +20,18 @@ export const assetUploadOption: MulterOptions = { | |||||||
|   storage: diskStorage({ |   storage: diskStorage({ | ||||||
|     destination: (req: Request, file: Express.Multer.File, cb: any) => { |     destination: (req: Request, file: Express.Multer.File, cb: any) => { | ||||||
|       const basePath = APP_UPLOAD_LOCATION; |       const basePath = APP_UPLOAD_LOCATION; | ||||||
|       const fileInfo = req.body as CreateAssetDto; |       // TODO these are currently not used. Shall we remove them? | ||||||
|  |       // const fileInfo = req.body as CreateAssetDto; | ||||||
|  |  | ||||||
|       const yearInfo = new Date(fileInfo.createdAt).getFullYear(); |       // const yearInfo = new Date(fileInfo.createdAt).getFullYear(); | ||||||
|       const monthInfo = new Date(fileInfo.createdAt).getMonth(); |       // const monthInfo = new Date(fileInfo.createdAt).getMonth(); | ||||||
|  |  | ||||||
|  |       if (!req.user) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (file.fieldname == 'assetData') { |       if (file.fieldname == 'assetData') { | ||||||
|         const originalUploadFolder = `${basePath}/${req.user['id']}/original/${req.body['deviceId']}`; |         const originalUploadFolder = `${basePath}/${req.user.id}/original/${req.body['deviceId']}`; | ||||||
|  |  | ||||||
|         if (!existsSync(originalUploadFolder)) { |         if (!existsSync(originalUploadFolder)) { | ||||||
|           mkdirSync(originalUploadFolder, { recursive: true }); |           mkdirSync(originalUploadFolder, { recursive: true }); | ||||||
| @@ -35,7 +40,7 @@ export const assetUploadOption: MulterOptions = { | |||||||
|         // Save original to disk |         // Save original to disk | ||||||
|         cb(null, originalUploadFolder); |         cb(null, originalUploadFolder); | ||||||
|       } else if (file.fieldname == 'thumbnailData') { |       } else if (file.fieldname == 'thumbnailData') { | ||||||
|         const thumbnailUploadFolder = `${basePath}/${req.user['id']}/thumb/${req.body['deviceId']}`; |         const thumbnailUploadFolder = `${basePath}/${req.user.id}/thumb/${req.body['deviceId']}`; | ||||||
|  |  | ||||||
|         if (!existsSync(thumbnailUploadFolder)) { |         if (!existsSync(thumbnailUploadFolder)) { | ||||||
|           mkdirSync(thumbnailUploadFolder, { recursive: true }); |           mkdirSync(thumbnailUploadFolder, { recursive: true }); | ||||||
|   | |||||||
| @@ -17,8 +17,11 @@ export const profileImageUploadOption: MulterOptions = { | |||||||
|  |  | ||||||
|   storage: diskStorage({ |   storage: diskStorage({ | ||||||
|     destination: (req: Request, file: Express.Multer.File, cb: any) => { |     destination: (req: Request, file: Express.Multer.File, cb: any) => { | ||||||
|  |       if (!req.user) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|       const basePath = APP_UPLOAD_LOCATION; |       const basePath = APP_UPLOAD_LOCATION; | ||||||
|       const profileImageLocation = `${basePath}/${req.user['id']}/profile`; |       const profileImageLocation = `${basePath}/${req.user.id}/profile`; | ||||||
|  |  | ||||||
|       if (!existsSync(profileImageLocation)) { |       if (!existsSync(profileImageLocation)) { | ||||||
|         mkdirSync(profileImageLocation, { recursive: true }); |         mkdirSync(profileImageLocation, { recursive: true }); | ||||||
| @@ -28,7 +31,10 @@ export const profileImageUploadOption: MulterOptions = { | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     filename: (req: Request, file: Express.Multer.File, cb: any) => { |     filename: (req: Request, file: Express.Multer.File, cb: any) => { | ||||||
|       const userId = req.user['id']; |       if (!req.user) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const userId = req.user.id; | ||||||
|  |  | ||||||
|       cb(null, `${userId}${extname(file.originalname)}`); |       cb(null, `${userId}${extname(file.originalname)}`); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common'; | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; | ||||||
| import { UserEntity } from '@app/database/entities/user.entity'; | import { UserEntity } from '@app/database/entities/user.entity'; | ||||||
| // import { AuthUserDto } from './dto/auth-user.dto'; | // import { AuthUserDto } from './dto/auth-user.dto'; | ||||||
|  |  | ||||||
| export class AuthUserDto { | export class AuthUserDto { | ||||||
|   id: string; |   id!: string; | ||||||
|   email: string; |   email!: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => { | export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => { | ||||||
|   const req = ctx.switchToHttp().getRequest(); |   const req = ctx.switchToHttp().getRequest<{ user: UserEntity }>(); | ||||||
|  |  | ||||||
|   const { id, email } = req.user as UserEntity; |   const { id, email } = req.user; | ||||||
|  |  | ||||||
|   const authUser: any = { |   const authUser: AuthUserDto = { | ||||||
|     id: id.toString(), |     id: id.toString(), | ||||||
|     email, |     email, | ||||||
|   }; |   }; | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								server/apps/immich/src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								server/apps/immich/src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { UserResponseDto } from './api-v1/user/response-dto/user-response.dto'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   namespace Express { | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-empty-interface | ||||||
|  |     interface User extends UserResponseDto {} | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; | ||||||
| import { Reflector } from '@nestjs/core'; | import { Reflector } from '@nestjs/core'; | ||||||
| import { JwtService } from '@nestjs/jwt'; |  | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
| import { UserEntity } from '@app/database/entities/user.entity'; | import { UserEntity } from '@app/database/entities/user.entity'; | ||||||
| @@ -22,7 +21,14 @@ export class AdminRolesGuard implements CanActivate { | |||||||
|       const bearerToken = request.headers['authorization'].split(' ')[1]; |       const bearerToken = request.headers['authorization'].split(' ')[1]; | ||||||
|       const { userId } = await this.jwtService.validateToken(bearerToken); |       const { userId } = await this.jwtService.validateToken(bearerToken); | ||||||
|  |  | ||||||
|  |       if (!userId) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       const user = await this.userRepository.findOne(userId); |       const user = await this.userRepository.findOne(userId); | ||||||
|  |       if (!user) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       return user.isAdmin; |       return user.isAdmin; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ export class AppLoggerMiddleware implements NestMiddleware { | |||||||
|   private logger = new Logger('HTTP'); |   private logger = new Logger('HTTP'); | ||||||
|  |  | ||||||
|   use(request: Request, response: Response, next: NextFunction): void { |   use(request: Request, response: Response, next: NextFunction): void { | ||||||
|     const { ip, method, path: url, baseUrl } = request; |     const { ip, method, baseUrl } = request; | ||||||
|     const userAgent = request.get('user-agent') || ''; |     const userAgent = request.get('user-agent') || ''; | ||||||
|  |  | ||||||
|     response.on('close', () => { |     response.on('close', () => { | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import { InjectQueue, Process, Processor } from '@nestjs/bull'; | import { Process, Processor } from '@nestjs/bull'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Job, Queue } from 'bull'; |  | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||||
| import fs from 'fs'; | import fs from 'fs'; | ||||||
| import { Logger } from '@nestjs/common'; |  | ||||||
| import axios from 'axios'; |  | ||||||
| import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; | import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; | ||||||
|  | import { Job } from 'bull'; | ||||||
|  |  | ||||||
| @Processor('background-task') | @Processor('background-task') | ||||||
| export class BackgroundTaskProcessor { | export class BackgroundTaskProcessor { | ||||||
| @@ -18,9 +16,10 @@ export class BackgroundTaskProcessor { | |||||||
|     private smartInfoRepository: Repository<SmartInfoEntity>, |     private smartInfoRepository: Repository<SmartInfoEntity>, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|  |   // TODO: Should probably use constants / Interfaces for Queue names / data | ||||||
|   @Process('delete-file-on-disk') |   @Process('delete-file-on-disk') | ||||||
|   async deleteFileOnDisk(job) { |   async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) { | ||||||
|     const { assets }: { assets: AssetEntity[] } = job.data; |     const { assets } = job.data; | ||||||
|  |  | ||||||
|     for (const asset of assets) { |     for (const asset of assets) { | ||||||
|       fs.unlink(asset.originalPath, (err) => { |       fs.unlink(asset.originalPath, (err) => { | ||||||
| @@ -29,11 +28,14 @@ export class BackgroundTaskProcessor { | |||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       fs.unlink(asset.resizePath, (err) => { |       // TODO: what if there is no asset.resizePath. Should fail the Job? | ||||||
|         if (err) { |       if (asset.resizePath) { | ||||||
|           console.log('error deleting ', asset.originalPath); |         fs.unlink(asset.resizePath, (err) => { | ||||||
|         } |           if (err) { | ||||||
|       }); |             console.log('error deleting ', asset.originalPath); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,11 @@ import { JwtService } from '@nestjs/jwt'; | |||||||
| import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto'; | import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto'; | ||||||
| import { jwtSecret } from '../../constants/jwt.constant'; | import { jwtSecret } from '../../constants/jwt.constant'; | ||||||
|  |  | ||||||
|  | export type JwtValidationResult = { | ||||||
|  |   status: boolean; | ||||||
|  |   userId: string | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class ImmichJwtService { | export class ImmichJwtService { | ||||||
|   constructor(private jwtService: JwtService) {} |   constructor(private jwtService: JwtService) {} | ||||||
| @@ -13,11 +18,11 @@ export class ImmichJwtService { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async validateToken(accessToken: string) { |   public async validateToken(accessToken: string): Promise<JwtValidationResult> { | ||||||
|     try { |     try { | ||||||
|       const payload = await this.jwtService.verify(accessToken, { secret: jwtSecret }); |       const payload = await this.jwtService.verifyAsync<JwtPayloadDto>(accessToken, { secret: jwtSecret }); | ||||||
|       return { |       return { | ||||||
|         userId: payload['userId'], |         userId: payload.userId, | ||||||
|         status: true, |         status: true, | ||||||
|       }; |       }; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; | |||||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | import { TypeOrmModule } from '@nestjs/typeorm'; | ||||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||||
| import { ScheduleTasksService } from './schedule-tasks.service'; | import { ScheduleTasksService } from './schedule-tasks.service'; | ||||||
| import { MicroservicesModule } from '../../../../microservices/src/microservices.module'; |  | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [ |   imports: [ | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { UserModule } from '../src/api-v1/user/user.module'; | |||||||
| import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module'; | import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module'; | ||||||
| import { UserService } from '../src/api-v1/user/user.service'; | import { UserService } from '../src/api-v1/user/user.service'; | ||||||
| import { CreateUserDto } from '../src/api-v1/user/dto/create-user.dto'; | import { CreateUserDto } from '../src/api-v1/user/dto/create-user.dto'; | ||||||
| import { User } from '../src/api-v1/user/response-dto/user'; | import { UserResponseDto } from '../src/api-v1/user/response-dto/user-response.dto'; | ||||||
|  |  | ||||||
| function _createUser(userService: UserService, data: CreateUserDto) { | function _createUser(userService: UserService, data: CreateUserDto) { | ||||||
|   return userService.createUser(data); |   return userService.createUser(data); | ||||||
| @@ -44,7 +44,7 @@ describe('User', () => { | |||||||
|  |  | ||||||
|   describe('with auth', () => { |   describe('with auth', () => { | ||||||
|     let userService: UserService; |     let userService: UserService; | ||||||
|     let authUser: User; |     let authUser: UserResponseDto; | ||||||
|  |  | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|       const builder = Test.createTestingModule({ |       const builder = Test.createTestingModule({ | ||||||
|   | |||||||
| @@ -11,8 +11,6 @@ import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; | |||||||
| import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; | import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; | ||||||
| import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor'; | import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor'; | ||||||
| import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; | import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; | ||||||
| import { AssetModule } from '../../immich/src/api-v1/asset/asset.module'; |  | ||||||
| import { CommunicationGateway } from '../../immich/src/api-v1/communication/communication.gateway'; |  | ||||||
| import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module'; | import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module'; | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { InjectQueue, OnQueueActive, OnQueueCompleted, OnQueueWaiting, Process, Processor } from '@nestjs/bull'; | import { InjectQueue, Process, Processor } from '@nestjs/bull'; | ||||||
| import { Job, Queue } from 'bull'; | import { Job, Queue } from 'bull'; | ||||||
| import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; | import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|   | |||||||
| @@ -11,13 +11,12 @@ import { readFile } from 'fs/promises'; | |||||||
| import { Logger } from '@nestjs/common'; | import { Logger } from '@nestjs/common'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; | import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; | ||||||
| import { ConfigService } from '@nestjs/config'; |  | ||||||
| import ffmpeg from 'fluent-ffmpeg'; | import ffmpeg from 'fluent-ffmpeg'; | ||||||
| // import moment from 'moment'; | // import moment from 'moment'; | ||||||
|  |  | ||||||
| @Processor('metadata-extraction-queue') | @Processor('metadata-extraction-queue') | ||||||
| export class MetadataExtractionProcessor { | export class MetadataExtractionProcessor { | ||||||
|   private geocodingClient: GeocodeService; |   private geocodingClient?: GeocodeService; | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     @InjectRepository(AssetEntity) |     @InjectRepository(AssetEntity) | ||||||
| @@ -29,7 +28,7 @@ export class MetadataExtractionProcessor { | |||||||
|     @InjectRepository(SmartInfoEntity) |     @InjectRepository(SmartInfoEntity) | ||||||
|     private smartInfoRepository: Repository<SmartInfoEntity>, |     private smartInfoRepository: Repository<SmartInfoEntity>, | ||||||
|   ) { |   ) { | ||||||
|     if (process.env.ENABLE_MAPBOX == 'true') { |     if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) { | ||||||
|       this.geocodingClient = mapboxGeocoding({ |       this.geocodingClient = mapboxGeocoding({ | ||||||
|         accessToken: process.env.MAPBOX_KEY, |         accessToken: process.env.MAPBOX_KEY, | ||||||
|       }); |       }); | ||||||
| @@ -65,7 +64,7 @@ export class MetadataExtractionProcessor { | |||||||
|       newExif.longitude = exifData['longitude'] || null; |       newExif.longitude = exifData['longitude'] || null; | ||||||
|  |  | ||||||
|       // Reverse GeoCoding |       // Reverse GeoCoding | ||||||
|       if (process.env.ENABLE_MAPBOX && exifData['longitude'] && exifData['latitude']) { |       if (this.geocodingClient && exifData['longitude'] && exifData['latitude']) { | ||||||
|         const geoCodeInfo: MapiResponse = await this.geocodingClient |         const geoCodeInfo: MapiResponse = await this.geocodingClient | ||||||
|           .reverseGeocode({ |           .reverseGeocode({ | ||||||
|             query: [exifData['longitude'], exifData['latitude']], |             query: [exifData['longitude'], exifData['latitude']], | ||||||
| @@ -86,7 +85,7 @@ export class MetadataExtractionProcessor { | |||||||
|  |  | ||||||
|       await this.exifRepository.save(newExif); |       await this.exifRepository.save(newExif); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif'); |       Logger.error(`Error extracting EXIF ${String(e)}`, 'extractExif'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -128,7 +127,7 @@ export class MetadataExtractionProcessor { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`); |       Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ export class ThumbnailGeneratorProcessor { | |||||||
|       sharp(asset.originalPath) |       sharp(asset.originalPath) | ||||||
|         .resize(1440, 2560, { fit: 'inside' }) |         .resize(1440, 2560, { fit: 'inside' }) | ||||||
|         .jpeg() |         .jpeg() | ||||||
|         .toFile(jpegThumbnailPath, async (err, info) => { |         .toFile(jpegThumbnailPath, async (err) => { | ||||||
|           if (!err) { |           if (!err) { | ||||||
|             await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath }); |             await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath }); | ||||||
|  |  | ||||||
| @@ -65,7 +65,7 @@ export class ThumbnailGeneratorProcessor { | |||||||
|         .on('start', () => { |         .on('start', () => { | ||||||
|           Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail'); |           Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail'); | ||||||
|         }) |         }) | ||||||
|         .on('error', (error, b, c) => { |         .on('error', (error) => { | ||||||
|           Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail'); |           Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail'); | ||||||
|           // reject(); |           // reject(); | ||||||
|         }) |         }) | ||||||
| @@ -87,15 +87,18 @@ export class ThumbnailGeneratorProcessor { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Process({ name: 'generate-webp-thumbnail', concurrency: 2 }) |   @Process({ name: 'generate-webp-thumbnail', concurrency: 2 }) | ||||||
|   async generateWepbThumbnail(job: Job) { |   async generateWepbThumbnail(job: Job<{ asset: AssetEntity }>) { | ||||||
|     const { asset }: { asset: AssetEntity } = job.data; |     const { asset } = job.data; | ||||||
|  |  | ||||||
|  |     if (!asset.resizePath) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     const webpPath = asset.resizePath.replace('jpeg', 'webp'); |     const webpPath = asset.resizePath.replace('jpeg', 'webp'); | ||||||
|  |  | ||||||
|     sharp(asset.resizePath) |     sharp(asset.resizePath) | ||||||
|       .resize(250) |       .resize(250) | ||||||
|       .webp() |       .webp() | ||||||
|       .toFile(webpPath, (err, info) => { |       .toFile(webpPath, (err) => { | ||||||
|         if (!err) { |         if (!err) { | ||||||
|           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath }); |           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath }); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ export class VideoTranscodeProcessor { | |||||||
|         .on('start', () => { |         .on('start', () => { | ||||||
|           Logger.log('Start Converting Video', 'mp4Conversion'); |           Logger.log('Start Converting Video', 'mp4Conversion'); | ||||||
|         }) |         }) | ||||||
|         .on('error', (error, b, c) => { |         .on('error', (error) => { | ||||||
|           Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion'); |           Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion'); | ||||||
|           reject(); |           reject(); | ||||||
|         }) |         }) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Test, TestingModule } from '@nestjs/testing'; | import { Test, TestingModule } from '@nestjs/testing'; | ||||||
| import { INestApplication } from '@nestjs/common'; | import { INestApplication } from '@nestjs/common'; | ||||||
| import * as request from 'supertest'; | import request from 'supertest'; | ||||||
| import { MicroservicesModule } from './../src/microservices.module'; | import { MicroservicesModule } from './../src/microservices.module'; | ||||||
|  |  | ||||||
| describe('MicroservicesController (e2e)', () => { | describe('MicroservicesController (e2e)', () => { | ||||||
|   | |||||||
| @@ -5,23 +5,23 @@ import { UserAlbumEntity } from './user-album.entity'; | |||||||
| @Entity('albums') | @Entity('albums') | ||||||
| export class AlbumEntity { | export class AlbumEntity { | ||||||
|   @PrimaryGeneratedColumn('uuid') |   @PrimaryGeneratedColumn('uuid') | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   ownerId: string; |   ownerId!: string; | ||||||
|  |  | ||||||
|   @Column({ default: 'Untitled Album' }) |   @Column({ default: 'Untitled Album' }) | ||||||
|   albumName: string; |   albumName!: string; | ||||||
|  |  | ||||||
|   @CreateDateColumn({ type: 'timestamptz' }) |   @CreateDateColumn({ type: 'timestamptz' }) | ||||||
|   createdAt: string; |   createdAt!: string; | ||||||
|  |  | ||||||
|   @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true }) |   @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true}) | ||||||
|   albumThumbnailAssetId: string; |   albumThumbnailAssetId!: string | null; | ||||||
|  |  | ||||||
|   @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo) |   @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo) | ||||||
|   sharedUsers: UserAlbumEntity[]; |   sharedUsers?: UserAlbumEntity[]; | ||||||
|  |  | ||||||
|   @OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo) |   @OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo) | ||||||
|   assets: AssetAlbumEntity[]; |   assets?: AssetAlbumEntity[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,25 +6,25 @@ import { AssetEntity } from './asset.entity'; | |||||||
| @Unique('PK_unique_asset_in_album', ['albumId', 'assetId']) | @Unique('PK_unique_asset_in_album', ['albumId', 'assetId']) | ||||||
| export class AssetAlbumEntity { | export class AssetAlbumEntity { | ||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   albumId: string; |   albumId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   assetId: string; |   assetId!: string; | ||||||
|  |  | ||||||
|   @ManyToOne(() => AlbumEntity, (album) => album.assets, { |   @ManyToOne(() => AlbumEntity, (album) => album.assets, { | ||||||
|     onDelete: 'CASCADE', |     onDelete: 'CASCADE', | ||||||
|     nullable: true, |     nullable: true, | ||||||
|   }) |   }) | ||||||
|   @JoinColumn({ name: 'albumId' }) |   @JoinColumn({ name: 'albumId' }) | ||||||
|   albumInfo: AlbumEntity; |   albumInfo!: AlbumEntity; | ||||||
|  |  | ||||||
|   @ManyToOne(() => AssetEntity, { |   @ManyToOne(() => AssetEntity, { | ||||||
|     onDelete: 'CASCADE', |     onDelete: 'CASCADE', | ||||||
|     nullable: true, |     nullable: true, | ||||||
|   }) |   }) | ||||||
|   @JoinColumn({ name: 'assetId' }) |   @JoinColumn({ name: 'assetId' }) | ||||||
|   assetInfo: AssetEntity; |   assetInfo!: AssetEntity; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm'; | import { Column, Entity, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; | ||||||
| import { ExifEntity } from './exif.entity'; | import { ExifEntity } from './exif.entity'; | ||||||
| import { SmartInfoEntity } from './smart-info.entity'; | import { SmartInfoEntity } from './smart-info.entity'; | ||||||
|  |  | ||||||
| @@ -6,52 +6,52 @@ import { SmartInfoEntity } from './smart-info.entity'; | |||||||
| @Unique(['deviceAssetId', 'userId', 'deviceId']) | @Unique(['deviceAssetId', 'userId', 'deviceId']) | ||||||
| export class AssetEntity { | export class AssetEntity { | ||||||
|   @PrimaryGeneratedColumn('uuid') |   @PrimaryGeneratedColumn('uuid') | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   deviceAssetId: string; |   deviceAssetId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   userId: string; |   userId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   deviceId: string; |   deviceId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   type: AssetType; |   type!: AssetType; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   originalPath: string; |   originalPath!: string; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   resizePath: string; |   resizePath!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   webpPath: string; |   webpPath!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   encodedVideoPath: string; |   encodedVideoPath!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   createdAt: string; |   createdAt!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   modifiedAt: string; |   modifiedAt!: string; | ||||||
|  |  | ||||||
|   @Column({ type: 'boolean', default: false }) |   @Column({ type: 'boolean', default: false }) | ||||||
|   isFavorite: boolean; |   isFavorite!: boolean; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   mimeType: string; |   mimeType!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   duration: string; |   duration!: string | null; | ||||||
|  |  | ||||||
|   @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset) |   @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset) | ||||||
|   exifInfo: ExifEntity; |   exifInfo?: ExifEntity; | ||||||
|  |  | ||||||
|   @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset) |   @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset) | ||||||
|   smartInfo: SmartInfoEntity; |   smartInfo?: SmartInfoEntity; | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum AssetType { | export enum AssetType { | ||||||
|   | |||||||
| @@ -4,25 +4,25 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from | |||||||
| @Unique(['userId', 'deviceId']) | @Unique(['userId', 'deviceId']) | ||||||
| export class DeviceInfoEntity { | export class DeviceInfoEntity { | ||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|   id: number; |   id!: number; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   userId: string; |   userId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   deviceId: string; |   deviceId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   deviceType: DeviceType; |   deviceType!: DeviceType; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   notificationToken: string; |   notificationToken!: string | null; | ||||||
|  |  | ||||||
|   @CreateDateColumn() |   @CreateDateColumn() | ||||||
|   createdAt: string; |   createdAt!: string; | ||||||
|  |  | ||||||
|   @Column({ type: 'bool', default: false }) |   @Column({ type: 'bool', default: false }) | ||||||
|   isAutoBackup: boolean; |   isAutoBackup!: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum DeviceType { | export enum DeviceType { | ||||||
|   | |||||||
| @@ -7,70 +7,70 @@ import { AssetEntity } from './asset.entity'; | |||||||
| @Entity('exif') | @Entity('exif') | ||||||
| export class ExifEntity { | export class ExifEntity { | ||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Index({ unique: true }) |   @Index({ unique: true }) | ||||||
|   @Column({ type: 'uuid' }) |   @Column({ type: 'uuid' }) | ||||||
|   assetId: string; |   assetId!: string; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   make: string; |   make!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   model: string; |   model!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   imageName: string; |   imageName!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'integer', nullable: true }) | ||||||
|   exifImageWidth: number; |   exifImageWidth!: number | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'integer', nullable: true }) | ||||||
|   exifImageHeight: number; |   exifImageHeight!: number | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'integer', nullable: true }) | ||||||
|   fileSizeInByte: number; |   fileSizeInByte!: number | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   orientation: string; |   orientation!: string | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'timestamptz', nullable: true }) |   @Column({ type: 'timestamptz', nullable: true }) | ||||||
|   dateTimeOriginal: Date; |   dateTimeOriginal!: Date | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'timestamptz', nullable: true }) |   @Column({ type: 'timestamptz', nullable: true }) | ||||||
|   modifyDate: Date; |   modifyDate!: Date | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   lensModel: string; |   lensModel!: string | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'float8', nullable: true }) |   @Column({ type: 'float8', nullable: true }) | ||||||
|   fNumber: number; |   fNumber!: number | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'float8', nullable: true }) |   @Column({ type: 'float8', nullable: true }) | ||||||
|   focalLength: number; |   focalLength!: number | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'integer', nullable: true }) | ||||||
|   iso: number; |   iso!: number | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'float', nullable: true }) |   @Column({ type: 'float', nullable: true }) | ||||||
|   exposureTime: number; |   exposureTime!: number | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'float', nullable: true }) |   @Column({ type: 'float', nullable: true }) | ||||||
|   latitude: number; |   latitude!: number | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'float', nullable: true }) |   @Column({ type: 'float', nullable: true }) | ||||||
|   longitude: number; |   longitude!: number | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   city: string; |   city!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   state: string; |   state!: string | null; | ||||||
|  |  | ||||||
|   @Column({ nullable: true }) |   @Column({ type: 'varchar', nullable: true }) | ||||||
|   country: string; |   country!: string | null; | ||||||
|  |  | ||||||
|   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) |   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) | ||||||
|   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) |   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) | ||||||
|   asset: ExifEntity; |   asset?: ExifEntity; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,19 +4,19 @@ import { AssetEntity } from './asset.entity'; | |||||||
| @Entity('smart_info') | @Entity('smart_info') | ||||||
| export class SmartInfoEntity { | export class SmartInfoEntity { | ||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Index({ unique: true }) |   @Index({ unique: true }) | ||||||
|   @Column({ type: 'uuid' }) |   @Column({ type: 'uuid' }) | ||||||
|   assetId: string; |   assetId!: string; | ||||||
|  |  | ||||||
|   @Column({ type: 'text', array: true, nullable: true }) |   @Column({ type: 'text', array: true, nullable: true }) | ||||||
|   tags: string[]; |   tags!: string[] | null; | ||||||
|  |  | ||||||
|   @Column({ type: 'text', array: true, nullable: true }) |   @Column({ type: 'text', array: true, nullable: true }) | ||||||
|   objects: string[]; |   objects!: string[] | null; | ||||||
|  |  | ||||||
|   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) |   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) | ||||||
|   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) |   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) | ||||||
|   asset: SmartInfoEntity; |   asset?: SmartInfoEntity; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,22 +6,22 @@ import { AlbumEntity } from './album.entity'; | |||||||
| @Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId']) | @Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId']) | ||||||
| export class UserAlbumEntity { | export class UserAlbumEntity { | ||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   albumId: string; |   albumId!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   sharedUserId: string; |   sharedUserId!: string; | ||||||
|  |  | ||||||
|   @ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, { |   @ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, { | ||||||
|     onDelete: 'CASCADE', |     onDelete: 'CASCADE', | ||||||
|     nullable: true, |     nullable: true, | ||||||
|   }) |   }) | ||||||
|   @JoinColumn({ name: 'albumId' }) |   @JoinColumn({ name: 'albumId' }) | ||||||
|   albumInfo: AlbumEntity; |   albumInfo!: AlbumEntity; | ||||||
|  |  | ||||||
|   @ManyToOne(() => UserEntity) |   @ManyToOne(() => UserEntity) | ||||||
|   @JoinColumn({ name: 'sharedUserId' }) |   @JoinColumn({ name: 'sharedUserId' }) | ||||||
|   userInfo: UserEntity; |   userInfo!: UserEntity; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,32 +3,32 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeor | |||||||
| @Entity('users') | @Entity('users') | ||||||
| export class UserEntity { | export class UserEntity { | ||||||
|   @PrimaryGeneratedColumn('uuid') |   @PrimaryGeneratedColumn('uuid') | ||||||
|   id: string; |   id!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   firstName: string; |   firstName!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   lastName: string; |   lastName!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   isAdmin: boolean; |   isAdmin!: boolean; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   email: string; |   email!: string; | ||||||
|  |  | ||||||
|   @Column({ select: false }) |   @Column({ select: false }) | ||||||
|   password: string; |   password?: string; | ||||||
|  |  | ||||||
|   @Column({ select: false }) |   @Column({ select: false }) | ||||||
|   salt: string; |   salt?: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   profileImagePath: string; |   profileImagePath!: string; | ||||||
|  |  | ||||||
|   @Column() |   @Column() | ||||||
|   isFirstLoggedIn: boolean; |   isFirstLoggedIn!: boolean; | ||||||
|  |  | ||||||
|   @CreateDateColumn() |   @CreateDateColumn() | ||||||
|   createdAt: string; |   createdAt!: string; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										53
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -59,6 +59,7 @@ | |||||||
|         "@types/imagemin": "^8.0.0", |         "@types/imagemin": "^8.0.0", | ||||||
|         "@types/jest": "27.0.2", |         "@types/jest": "27.0.2", | ||||||
|         "@types/lodash": "^4.14.178", |         "@types/lodash": "^4.14.178", | ||||||
|  |         "@types/mapbox__mapbox-sdk": "^0.13.4", | ||||||
|         "@types/multer": "^1.4.7", |         "@types/multer": "^1.4.7", | ||||||
|         "@types/node": "^16.0.0", |         "@types/node": "^16.0.0", | ||||||
|         "@types/passport-jwt": "^3.0.6", |         "@types/passport-jwt": "^3.0.6", | ||||||
| @@ -2205,6 +2206,12 @@ | |||||||
|         "@types/node": "*" |         "@types/node": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@types/geojson": { | ||||||
|  |       "version": "7946.0.8", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", | ||||||
|  |       "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", | ||||||
|  |       "dev": true | ||||||
|  |     }, | ||||||
|     "node_modules/@types/graceful-fs": { |     "node_modules/@types/graceful-fs": { | ||||||
|       "version": "4.1.5", |       "version": "4.1.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", |       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", | ||||||
| @@ -2312,6 +2319,26 @@ | |||||||
|       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", |       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@types/mapbox__mapbox-sdk": { | ||||||
|  |       "version": "0.13.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz", | ||||||
|  |       "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/geojson": "*", | ||||||
|  |         "@types/mapbox-gl": "*", | ||||||
|  |         "@types/node": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@types/mapbox-gl": { | ||||||
|  |       "version": "2.7.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz", | ||||||
|  |       "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/geojson": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@types/mime": { |     "node_modules/@types/mime": { | ||||||
|       "version": "1.3.2", |       "version": "1.3.2", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", |       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", | ||||||
| @@ -12822,6 +12849,12 @@ | |||||||
|         "@types/node": "*" |         "@types/node": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "@types/geojson": { | ||||||
|  |       "version": "7946.0.8", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", | ||||||
|  |       "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", | ||||||
|  |       "dev": true | ||||||
|  |     }, | ||||||
|     "@types/graceful-fs": { |     "@types/graceful-fs": { | ||||||
|       "version": "4.1.5", |       "version": "4.1.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", |       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", | ||||||
| @@ -12929,6 +12962,26 @@ | |||||||
|       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", |       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "@types/mapbox__mapbox-sdk": { | ||||||
|  |       "version": "0.13.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz", | ||||||
|  |       "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "requires": { | ||||||
|  |         "@types/geojson": "*", | ||||||
|  |         "@types/mapbox-gl": "*", | ||||||
|  |         "@types/node": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "@types/mapbox-gl": { | ||||||
|  |       "version": "2.7.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz", | ||||||
|  |       "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==", | ||||||
|  |       "dev": true, | ||||||
|  |       "requires": { | ||||||
|  |         "@types/geojson": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "@types/mime": { |     "@types/mime": { | ||||||
|       "version": "1.3.2", |       "version": "1.3.2", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", |       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", | ||||||
|   | |||||||
| @@ -13,7 +13,10 @@ | |||||||
|     "start:dev": "nest start --watch", |     "start:dev": "nest start --watch", | ||||||
|     "start:debug": "nest start --debug --watch", |     "start:debug": "nest start --debug --watch", | ||||||
|     "start:prod": "node dist/main", |     "start:prod": "node dist/main", | ||||||
|     "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", |     "lint": "eslint \"{apps,libs}/**/*.ts\" --max-warnings 0", | ||||||
|  |     "lint:fix": "npm run lint -- --fix", | ||||||
|  |     "check:types": "tsc --noEmit", | ||||||
|  |     "check:all": "npm run lint && npm run check:types && npm run test", | ||||||
|     "test": "jest", |     "test": "jest", | ||||||
|     "test:watch": "jest --watch", |     "test:watch": "jest --watch", | ||||||
|     "test:cov": "jest --coverage", |     "test:cov": "jest --coverage", | ||||||
| @@ -72,6 +75,7 @@ | |||||||
|     "@types/imagemin": "^8.0.0", |     "@types/imagemin": "^8.0.0", | ||||||
|     "@types/jest": "27.0.2", |     "@types/jest": "27.0.2", | ||||||
|     "@types/lodash": "^4.14.178", |     "@types/lodash": "^4.14.178", | ||||||
|  |     "@types/mapbox__mapbox-sdk": "^0.13.4", | ||||||
|     "@types/multer": "^1.4.7", |     "@types/multer": "^1.4.7", | ||||||
|     "@types/node": "^16.0.0", |     "@types/node": "^16.0.0", | ||||||
|     "@types/passport-jwt": "^3.0.6", |     "@types/passport-jwt": "^3.0.6", | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "module": "commonjs", |     "module": "commonjs", | ||||||
|  |     "strict": true, | ||||||
|     "declaration": true, |     "declaration": true, | ||||||
|     "removeComments": true, |     "removeComments": true, | ||||||
|     "emitDecoratorMetadata": true, |     "emitDecoratorMetadata": true, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user