From c918f5b001c3ed29654512d5eeeba7e22f72b34d Mon Sep 17 00:00:00 2001 From: Jaime Baez Date: Sat, 25 Jun 2022 19:53:06 +0200 Subject: [PATCH] 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. --- .../src/api-v1/album/album-repository.ts | 11 ++- .../src/api-v1/album/album.service.spec.ts | 6 +- .../immich/src/api-v1/album/album.service.ts | 2 +- .../src/api-v1/album/dto/add-assets.dto.ts | 2 +- .../src/api-v1/album/dto/add-users.dto.ts | 2 +- .../src/api-v1/album/dto/create-album.dto.ts | 2 +- .../src/api-v1/album/dto/remove-assets.dto.ts | 2 +- .../src/api-v1/album/dto/update-album.dto.ts | 4 +- .../album/response-dto/album-response.dto.ts | 4 +- .../src/api-v1/asset/asset.controller.ts | 21 +++--- .../immich/src/api-v1/asset/asset.service.ts | 62 +++++++++++++---- .../src/api-v1/asset/dto/create-asset.dto.ts | 16 ++--- .../src/api-v1/asset/dto/create-exif.dto.ts | 30 ++++---- .../src/api-v1/asset/dto/delete-asset.dto.ts | 2 +- .../asset/dto/get-all-asset-query.dto.ts | 4 +- .../asset/dto/get-all-asset-response.dto.ts | 7 +- .../src/api-v1/asset/dto/get-asset.dto.ts | 2 +- .../asset/dto/get-new-asset-query.dto.ts | 2 +- .../src/api-v1/asset/dto/search-asset.dto.ts | 2 +- .../src/api-v1/asset/dto/serve-file.dto.ts | 11 ++- .../asset/response-dto/asset-response.dto.ts | 4 +- .../immich/src/api-v1/auth/auth.controller.ts | 1 + .../immich/src/api-v1/auth/auth.service.ts | 7 +- .../api-v1/auth/dto/login-credential.dto.ts | 4 +- .../immich/src/api-v1/auth/dto/sign-up.dto.ts | 8 +-- .../communication/communication.gateway.ts | 14 ++-- .../device-info/device-info.controller.ts | 2 +- .../api-v1/device-info/device-info.service.ts | 2 +- .../device-info/dto/create-device-info.dto.ts | 6 +- .../device-info/dto/update-device-info.dto.ts | 2 - .../api-v1/server-info/dto/server-info.dto.ts | 15 ++-- .../src/api-v1/user/dto/create-user.dto.ts | 8 +-- .../{user.ts => user-response.dto.ts} | 4 +- .../immich/src/api-v1/user/user.controller.ts | 2 - .../immich/src/api-v1/user/user.service.ts | 20 +++++- server/apps/immich/src/app.controller.ts | 7 +- server/apps/immich/src/app.module.ts | 5 +- .../immich/src/config/asset-upload.config.ts | 17 +++-- .../src/config/profile-image-upload.config.ts | 10 ++- .../src/decorators/auth-user.decorator.ts | 12 ++-- server/apps/immich/src/global.d.ts | 8 +++ .../admin-role-guard.middleware.ts | 8 ++- .../src/middlewares/app-logger.middleware.ts | 2 +- .../background-task.processor.ts | 24 ++++--- .../modules/immich-jwt/immich-jwt.service.ts | 11 ++- .../schedule-tasks/schedule-tasks.module.ts | 1 - server/apps/immich/test/user.e2e-spec.ts | 4 +- .../microservices/src/microservices.module.ts | 2 - .../processors/asset-uploaded.processor.ts | 2 +- .../metadata-extraction.processor.ts | 11 ++- .../src/processors/thumbnail.processor.ts | 13 ++-- .../processors/video-transcode.processor.ts | 2 +- .../apps/microservices/test/app.e2e-spec.ts | 2 +- .../database/src/entities/album.entity.ts | 16 ++--- .../src/entities/asset-album.entity.ts | 10 +-- .../database/src/entities/asset.entity.ts | 44 ++++++------ .../src/entities/device-info.entity.ts | 16 ++--- .../libs/database/src/entities/exif.entity.ts | 68 +++++++++---------- .../src/entities/smart-info.entity.ts | 10 +-- .../src/entities/user-album.entity.ts | 10 +-- .../libs/database/src/entities/user.entity.ts | 20 +++--- server/package-lock.json | 53 +++++++++++++++ server/package.json | 6 +- server/tsconfig.json | 1 + 64 files changed, 415 insertions(+), 273 deletions(-) rename server/apps/immich/src/api-v1/user/response-dto/{user.ts => user-response.dto.ts} (77%) create mode 100644 server/apps/immich/src/global.d.ts diff --git a/server/apps/immich/src/api-v1/album/album-repository.ts b/server/apps/immich/src/api-v1/album/album-repository.ts index 3b33c2f225..152cebf75d 100644 --- a/server/apps/immich/src/api-v1/album/album-repository.ts +++ b/server/apps/immich/src/api-v1/album/album-repository.ts @@ -14,7 +14,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto'; export interface IAlbumRepository { create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise; getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise; - get(albumId: string): Promise; + get(albumId: string): Promise; delete(album: AlbumEntity): Promise; addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise; removeUser(album: AlbumEntity, userId: string): Promise; @@ -39,7 +39,7 @@ export class AlbumRepository implements IAlbumRepository { ) {} async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise { - return await getConnection().transaction(async (transactionalEntityManager) => { + return getConnection().transaction(async (transactionalEntityManager) => { // Create album entity const newAlbum = new AlbumEntity(); newAlbum.ownerId = ownerId; @@ -80,7 +80,6 @@ export class AlbumRepository implements IAlbumRepository { return album; }); - return; } getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise { @@ -155,7 +154,7 @@ export class AlbumRepository implements IAlbumRepository { return; } // 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(), ); @@ -180,7 +179,7 @@ export class AlbumRepository implements IAlbumRepository { } await this.userAlbumRepository.save([...newRecords]); - return this.get(album.id); + return this.get(album.id) as Promise; // There is an album for sure } async removeUser(album: AlbumEntity, userId: string): Promise { @@ -217,7 +216,7 @@ export class AlbumRepository implements IAlbumRepository { } await this.assetAlbumRepository.save([...newRecords]); - return this.get(album.id); + return this.get(album.id) as Promise; // There is an album for sure } updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise { diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 7a671fc720..985c3cb8fb 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -25,6 +25,7 @@ describe('Album service', () => { albumEntity.createdAt = 'date'; albumEntity.sharedUsers = []; albumEntity.assets = []; + albumEntity.albumThumbnailAssetId = null; return albumEntity; }; @@ -36,6 +37,7 @@ describe('Album service', () => { albumEntity.albumName = 'name'; albumEntity.createdAt = 'date'; albumEntity.assets = []; + albumEntity.albumThumbnailAssetId = null; albumEntity.sharedUsers = [ { id: '99', @@ -60,6 +62,7 @@ describe('Album service', () => { albumEntity.albumName = 'name'; albumEntity.createdAt = 'date'; albumEntity.assets = []; + albumEntity.albumThumbnailAssetId = null; albumEntity.sharedUsers = [ { id: '99', @@ -96,6 +99,7 @@ describe('Album service', () => { albumEntity.createdAt = 'date'; albumEntity.sharedUsers = []; albumEntity.assets = []; + albumEntity.albumThumbnailAssetId = null; return albumEntity; }; @@ -151,7 +155,7 @@ describe('Album service', () => { const expectedResult: AlbumResponseDto = { albumName: 'name', - albumThumbnailAssetId: undefined, + albumThumbnailAssetId: null, createdAt: 'date', id: '0001', ownerId, diff --git a/server/apps/immich/src/api-v1/album/album.service.ts b/server/apps/immich/src/api-v1/album/album.service.ts index d5ee4ea763..9b87dac01b 100644 --- a/server/apps/immich/src/api-v1/album/album.service.ts +++ b/server/apps/immich/src/api-v1/album/album.service.ts @@ -31,7 +31,7 @@ export class AlbumService { if (validateIsOwner && !isOwner) { 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'); } return album; diff --git a/server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts b/server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts index 8bfbd05fd5..18b29f7c88 100644 --- a/server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class AddAssetsDto { @IsNotEmpty() - assetIds: string[]; + assetIds!: string[]; } diff --git a/server/apps/immich/src/api-v1/album/dto/add-users.dto.ts b/server/apps/immich/src/api-v1/album/dto/add-users.dto.ts index b3f44ba4c3..6a386053ad 100644 --- a/server/apps/immich/src/api-v1/album/dto/add-users.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/add-users.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class AddUsersDto { @IsNotEmpty() - sharedUserIds: string[]; + sharedUserIds!: string[]; } diff --git a/server/apps/immich/src/api-v1/album/dto/create-album.dto.ts b/server/apps/immich/src/api-v1/album/dto/create-album.dto.ts index 2f3a041752..f6b1e94764 100644 --- a/server/apps/immich/src/api-v1/album/dto/create-album.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/create-album.dto.ts @@ -2,7 +2,7 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; export class CreateAlbumDto { @IsNotEmpty() - albumName: string; + albumName!: string; @IsOptional() sharedWithUserIds?: string[]; diff --git a/server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts b/server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts index dd6943e52c..73f2994d96 100644 --- a/server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class RemoveAssetsDto { @IsNotEmpty() - assetIds: string[]; + assetIds!: string[]; } diff --git a/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts b/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts index 8966fff91b..7b9e7189ab 100644 --- a/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts @@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator'; export class UpdateAlbumDto { @IsNotEmpty() - albumName: string; + albumName!: string; @IsNotEmpty() - ownerId: string; + ownerId!: string; } diff --git a/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts b/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts index 1f04c71ddf..0a2ec33e72 100644 --- a/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts +++ b/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts @@ -1,5 +1,5 @@ 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'; export interface AlbumResponseDto { @@ -9,7 +9,7 @@ export interface AlbumResponseDto { createdAt: string; albumThumbnailAssetId: string | null; shared: boolean; - sharedUsers: User[]; + sharedUsers: UserResponseDto[]; assets: AssetResponseDto[]; } diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index e247d778d8..2677b358e6 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -14,7 +14,6 @@ import { Headers, Delete, Logger, - Patch, HttpCode, } from '@nestjs/common'; 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 { ServeFileDto } from './dto/serve-file.dto'; import { AssetEntity } from '@app/database/entities/asset.entity'; -import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; 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 { DeleteAssetDto } from './dto/delete-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto'; @@ -58,15 +55,18 @@ export class AssetController { ), ) async uploadFile( - @GetAuthUser() authUser, + @GetAuthUser() authUser: AuthUserDto, @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] }, @Body(ValidationPipe) assetInfo: CreateAssetDto, - ) { + ): Promise<'ok' | undefined> { for (const file of uploadFiles.assetData) { try { const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); - if (uploadFiles.thumbnailData != null && savedAsset) { + if (!savedAsset) { + return; + } + if (uploadFiles.thumbnailData != null) { const assetWithThumbnail = await this.assetService.updateThumbnailInfo( savedAsset, uploadFiles.thumbnailData[0].path, @@ -107,11 +107,11 @@ export class AssetController { @Get('/file') async serveFile( - @Headers() headers, + @Headers() headers: Record, @GetAuthUser() authUser: AuthUserDto, @Response({ passthrough: true }) res: Res, @Query(ValidationPipe) query: ServeFileDto, - ): Promise { + ): Promise { return this.assetService.serveFile(authUser, query, res, headers); } @@ -151,7 +151,7 @@ export class AssetController { } @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); } @@ -161,6 +161,9 @@ export class AssetController { for (const id of assetIds.ids) { const assets = await this.assetService.getAssetById(authUser, id); + if (!assets) { + continue; + } deleteAssetList.push(assets); } diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 59743cf023..2aa3ffc56c 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -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 { IsNull, Not, Repository } from 'typeorm'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { CreateAssetDto } from './dto/create-asset.dto'; import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; -import _ from 'lodash'; import { createReadStream, stat } from 'fs'; import { ServeFileDto } from './dto/serve-file.dto'; import { Response as Res } from 'express'; @@ -33,7 +39,12 @@ export class AssetService { 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 { const asset = new AssetEntity(); asset.deviceAssetId = assetInfo.deviceAssetId; asset.userId = authUser.id; @@ -44,10 +55,14 @@ export class AssetService { asset.modifiedAt = assetInfo.modifiedAt; asset.isFavorite = assetInfo.isFavorite; asset.mimeType = mimeType; - asset.duration = assetInfo.duration; + asset.duration = assetInfo.duration || null; 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) { Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); } @@ -62,7 +77,7 @@ export class AssetService { select: ['deviceAssetId'], }); - const res = []; + const res: string[] = []; rows.forEach((v) => res.push(v.deviceAssetId)); return res; } @@ -119,6 +134,9 @@ export class AssetService { }); file = createReadStream(asset.originalPath); } else { + if (!asset.resizePath) { + throw new Error('resizePath not set'); + } const { size } = await fileInfo(asset.resizePath); res.set({ 'Content-Type': 'image/jpeg', @@ -134,16 +152,25 @@ export class AssetService { } } - public async getAssetThumbnail(assetId: string) { + public async getAssetThumbnail(assetId: string): Promise { try { const asset = await this.assetRepository.findOne({ id: assetId }); + if (!asset) { + throw new NotFoundException('Asset not found'); + } if (asset.webpPath && asset.webpPath.length > 0) { return new StreamableFile(createReadStream(asset.webpPath)); } else { + if (!asset.resizePath) { + throw new Error('resizePath not set'); + } return new StreamableFile(createReadStream(asset.resizePath)); } } catch (e) { + if (e instanceof NotFoundException) { + throw e; + } Logger.error('Error serving asset thumbnail ', e); 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); if (!asset) { + // TODO: maybe this should be a NotFoundException? throw new BadRequestException('Asset does not exist'); } @@ -166,6 +194,10 @@ export class AssetService { res.set({ '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)); } @@ -189,6 +221,9 @@ export class AssetService { res.set({ 'Content-Type': 'image/jpeg', }); + if (!asset.resizePath) { + throw new Error('resizePath not set'); + } file = createReadStream(asset.resizePath); } } @@ -297,6 +332,7 @@ export class AssetService { async getAssetSearchTerm(authUser: AuthUserDto): Promise { const possibleSearchTerm = new Set(); + // TODO: should use query builder 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 @@ -308,12 +344,12 @@ export class AssetService { [authUser.id], ); - rows.forEach((row) => { + rows.forEach((row: { [x: string]: any }) => { // tags - row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); + row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); // objects - row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase())); + row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase())); // asset's tyoe possibleSearchTerm.add(row['type']?.toLowerCase()); @@ -345,7 +381,7 @@ export class AssetService { LEFT JOIN exif e ON a.id = e."assetId" 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.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" from assets a left join exif e on a.id = e."assetId" - where a."userId" = $1 + where a."userId" = $1 and e.city is not null 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" from assets a left join smart_info si on a.id = si."assetId" - where a."userId" = $1 + where a."userId" = $1 and si.objects is not null `, [authUser.id], diff --git a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts index 80ebdfd437..f1194ec4de 100644 --- a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts @@ -3,26 +3,26 @@ import { AssetType } from '@app/database/entities/asset.entity'; export class CreateAssetDto { @IsNotEmpty() - deviceAssetId: string; + deviceAssetId!: string; @IsNotEmpty() - deviceId: string; + deviceId!: string; @IsNotEmpty() - assetType: AssetType; + assetType!: AssetType; @IsNotEmpty() - createdAt: string; + createdAt!: string; @IsNotEmpty() - modifiedAt: string; + modifiedAt!: string; @IsNotEmpty() - isFavorite: boolean; + isFavorite!: boolean; @IsNotEmpty() - fileExtension: string; + fileExtension!: string; @IsOptional() - duration: string; + duration?: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts index 0214c1ee57..16a7d6f226 100644 --- a/server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts @@ -2,47 +2,47 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; export class CreateExifDto { @IsNotEmpty() - assetId: string; + assetId!: string; @IsOptional() - make: string; + make?: string; @IsOptional() - model: string; + model?: string; @IsOptional() - imageName: string; + imageName?: string; @IsOptional() - exifImageWidth: number; + exifImageWidth?: number; @IsOptional() - exifImageHeight: number; + exifImageHeight?: number; @IsOptional() - fileSizeInByte: number; + fileSizeInByte?: number; @IsOptional() - orientation: string; + orientation?: string; @IsOptional() - dateTimeOriginal: Date; + dateTimeOriginal?: Date; @IsOptional() - modifiedDate: Date; + modifiedDate?: Date; @IsOptional() - lensModel: string; + lensModel?: string; @IsOptional() - fNumber: number; + fNumber?: number; @IsOptional() - focalLenght: number; + focalLenght?: number; @IsOptional() - iso: number; + iso?: number; @IsOptional() - exposureTime: number; + exposureTime?: number; } diff --git a/server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts index 0304dd5a21..7bed48455f 100644 --- a/server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class DeleteAssetDto { @IsNotEmpty() - ids: string[]; + ids!: string[]; } diff --git a/server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts index b3c235c098..67f5e319fa 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts @@ -1,6 +1,6 @@ -import { IsNotEmpty, IsOptional } from 'class-validator'; +import { IsOptional } from 'class-validator'; export class GetAllAssetQueryDto { @IsOptional() - nextPageKey: string; + nextPageKey?: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts index 6118786e9e..5af3e205aa 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts @@ -1,7 +1,8 @@ import { AssetEntity } from '@app/database/entities/asset.entity'; +// TODO: this doesn't seem to be used export class GetAllAssetReponseDto { - data: Array<{ date: string; assets: Array }>; - count: number; - nextPageKey: string; + data!: Array<{ date: string; assets: Array }>; + count!: number; + nextPageKey!: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts index fa598e2fa8..d3b27c1c0b 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class GetAssetDto { @IsNotEmpty() - deviceId: string; + deviceId!: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts index f10b3bec80..2d98f0fb59 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class GetNewAssetQueryDto { @IsNotEmpty() - latestDate: string; + latestDate!: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts index aeca2c0443..83a34239d4 100644 --- a/server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts @@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator'; export class SearchAssetDto { @IsNotEmpty() - searchTerm: string; + searchTerm!: string; } diff --git a/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts b/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts index a4244faee4..9a24035602 100644 --- a/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts @@ -1,20 +1,19 @@ -import { Transform } from 'class-transformer'; -import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator'; export class ServeFileDto { //assetId @IsNotEmpty() - aid: string; + aid!: string; //deviceId @IsNotEmpty() - did: string; + did!: string; @IsOptional() @IsBooleanString() - isThumb: string; + isThumb?: string; @IsOptional() @IsBooleanString() - isWeb: string; + isWeb?: string; } diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts index 6a83953216..6337405b3b 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts @@ -14,7 +14,7 @@ export interface AssetResponseDto { modifiedAt: string; isFavorite: boolean; mimeType: string | null; - duration: string | null; + duration: string; exifInfo?: ExifResponseDto; smartInfo?: SmartInfoResponseDto; } @@ -32,7 +32,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { modifiedAt: entity.modifiedAt, isFavorite: entity.isFavorite, mimeType: entity.mimeType, - duration: entity.duration, + duration: entity.duration ?? '0:00:00.00000', exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, }; diff --git a/server/apps/immich/src/api-v1/auth/auth.controller.ts b/server/apps/immich/src/api-v1/auth/auth.controller.ts index 16220f2250..d7942c4d9a 100644 --- a/server/apps/immich/src/api-v1/auth/auth.controller.ts +++ b/server/apps/immich/src/api-v1/auth/auth.controller.ts @@ -21,6 +21,7 @@ export class AuthController { @UseGuards(JwtAuthGuard) @Post('/validateToken') + // eslint-disable-next-line @typescript-eslint/no-unused-vars async validateToken(@GetAuthUser() authUser: AuthUserDto) { return { authStatus: true, diff --git a/server/apps/immich/src/api-v1/auth/auth.service.ts b/server/apps/immich/src/api-v1/auth/auth.service.ts index b55fdb7896..675acd2af2 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.ts @@ -7,7 +7,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { JwtPayloadDto } from './dto/jwt-payload.dto'; import { SignUpDto } from './dto/sign-up.dto'; import * as bcrypt from 'bcrypt'; -import { mapUser, User } from '../user/response-dto/user'; +import { mapUser, UserResponseDto } from '../user/response-dto/user-response.dto'; @Injectable() export class AuthService { @@ -39,7 +39,8 @@ export class AuthService { 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) { return user; @@ -69,7 +70,7 @@ export class AuthService { }; } - public async adminSignUp(signUpCredential: SignUpDto): Promise { + public async adminSignUp(signUpCredential: SignUpDto): Promise { const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } }); if (adminUser) { diff --git a/server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts b/server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts index 9147967674..da0077a6f7 100644 --- a/server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts +++ b/server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts @@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator'; export class LoginCredentialDto { @IsNotEmpty() - email: string; + email!: string; @IsNotEmpty() - password: string; + password!: string; } diff --git a/server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts b/server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts index c96bbc9645..b8f666adb8 100644 --- a/server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts +++ b/server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts @@ -2,14 +2,14 @@ import { IsNotEmpty } from 'class-validator'; export class SignUpDto { @IsNotEmpty() - email: string; + email!: string; @IsNotEmpty() - password: string; + password!: string; @IsNotEmpty() - firstName: string; + firstName!: string; @IsNotEmpty() - lastName: string; + lastName!: string; } diff --git a/server/apps/immich/src/api-v1/communication/communication.gateway.ts b/server/apps/immich/src/api-v1/communication/communication.gateway.ts index a7e06bb023..03f2e71abe 100644 --- a/server/apps/immich/src/api-v1/communication/communication.gateway.ts +++ b/server/apps/immich/src/api-v1/communication/communication.gateway.ts @@ -1,12 +1,10 @@ import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; -import { CommunicationService } from './communication.service'; 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 { InjectRepository } from '@nestjs/typeorm'; import { UserEntity } from '@app/database/entities/user.entity'; import { Repository } from 'typeorm'; -import { query } from 'express'; @WebSocketGateway({ cors: true }) export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -17,7 +15,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco private userRepository: Repository, ) {} - @WebSocketServer() server: Server; + @WebSocketServer() server!: Server; handleDisconnect(client: Socket) { client.leave(client.nsp.name); @@ -25,13 +23,15 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent'); } - async handleConnection(client: Socket, ...args: any[]) { + async handleConnection(client: Socket) { try { 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) { client.emit('error', 'unauthorized'); diff --git a/server/apps/immich/src/api-v1/device-info/device-info.controller.ts b/server/apps/immich/src/api-v1/device-info/device-info.controller.ts index 4f053c592b..61d3e9d050 100644 --- a/server/apps/immich/src/api-v1/device-info/device-info.controller.ts +++ b/server/apps/immich/src/api-v1/device-info/device-info.controller.ts @@ -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 { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { DeviceInfoService } from './device-info.service'; diff --git a/server/apps/immich/src/api-v1/device-info/device-info.service.ts b/server/apps/immich/src/api-v1/device-info/device-info.service.ts index 718774cbf1..2813a2ffc5 100644 --- a/server/apps/immich/src/api-v1/device-info/device-info.service.ts +++ b/server/apps/immich/src/api-v1/device-info/device-info.service.ts @@ -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 { Repository } from 'typeorm'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; diff --git a/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts b/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts index 04c7e0e190..a9db6ea435 100644 --- a/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts +++ b/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts @@ -3,11 +3,11 @@ import { DeviceType } from '@app/database/entities/device-info.entity'; export class CreateDeviceInfoDto { @IsNotEmpty() - deviceId: string; + deviceId!: string; @IsNotEmpty() - deviceType: DeviceType; + deviceType!: DeviceType; @IsOptional() - isAutoBackup: boolean; + isAutoBackup?: boolean; } diff --git a/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts b/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts index f8e954f8e8..cd2be701a7 100644 --- a/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts +++ b/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts @@ -1,6 +1,4 @@ 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'; export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {} diff --git a/server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts b/server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts index 6abdbf58ae..00cf7e4e68 100644 --- a/server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts @@ -1,9 +1,10 @@ +// TODO: this is being used as a response DTO. Should be changed to interface export class ServerInfoDto { - diskSize: string; - diskUse: string; - diskAvailable: string; - diskSizeRaw: number; - diskUseRaw: number; - diskAvailableRaw: number; - diskUsagePercentage: number; + diskSize!: string; + diskUse!: string; + diskAvailable!: string; + diskSizeRaw!: number; + diskUseRaw!: number; + diskAvailableRaw!: number; + diskUsagePercentage!: number; } diff --git a/server/apps/immich/src/api-v1/user/dto/create-user.dto.ts b/server/apps/immich/src/api-v1/user/dto/create-user.dto.ts index fbd63a22f4..e3ef6eb5ff 100644 --- a/server/apps/immich/src/api-v1/user/dto/create-user.dto.ts +++ b/server/apps/immich/src/api-v1/user/dto/create-user.dto.ts @@ -2,16 +2,16 @@ import { IsNotEmpty, IsOptional } from 'class-validator'; export class CreateUserDto { @IsNotEmpty() - email: string; + email!: string; @IsNotEmpty() - password: string; + password!: string; @IsNotEmpty() - firstName: string; + firstName!: string; @IsNotEmpty() - lastName: string; + lastName!: string; @IsOptional() profileImagePath?: string; diff --git a/server/apps/immich/src/api-v1/user/response-dto/user.ts b/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts similarity index 77% rename from server/apps/immich/src/api-v1/user/response-dto/user.ts rename to server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts index 57f503ac16..06bb11843f 100644 --- a/server/apps/immich/src/api-v1/user/response-dto/user.ts +++ b/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts @@ -1,6 +1,6 @@ import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity'; -export interface User { +export interface UserResponseDto { id: string; email: string; firstName: string; @@ -8,7 +8,7 @@ export interface User { createdAt: string; } -export function mapUser(entity: UserEntity): User { +export function mapUser(entity: UserEntity): UserResponseDto { return { id: entity.id, email: entity.email, diff --git a/server/apps/immich/src/api-v1/user/user.controller.ts b/server/apps/immich/src/api-v1/user/user.controller.ts index b7da4f87d3..a81de1301d 100644 --- a/server/apps/immich/src/api-v1/user/user.controller.ts +++ b/server/apps/immich/src/api-v1/user/user.controller.ts @@ -3,9 +3,7 @@ import { Get, Post, Body, - Patch, Param, - Delete, UseGuards, ValidationPipe, Put, diff --git a/server/apps/immich/src/api-v1/user/user.service.ts b/server/apps/immich/src/api-v1/user/user.service.ts index 3ce8d38531..d0ad2d0843 100644 --- a/server/apps/immich/src/api-v1/user/user.service.ts +++ b/server/apps/immich/src/api-v1/user/user.service.ts @@ -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 { Not, Repository } from 'typeorm'; 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 { createReadStream } from 'fs'; import { Response as Res } from 'express'; -import { mapUser, User } from './response-dto/user'; +import { mapUser, UserResponseDto } from './response-dto/user-response.dto'; @Injectable() export class UserService { @@ -44,7 +51,7 @@ export class UserService { }; } - async createUser(createUserDto: CreateUserDto): Promise { + async createUser(createUserDto: CreateUserDto): Promise { const user = await this.userRepository.findOne({ where: { email: createUserDto.email } }); if (user) { @@ -75,6 +82,9 @@ export class UserService { async updateUser(updateUserDto: UpdateUserDto) { const user = await this.userRepository.findOne(updateUserDto.id); + if (!user) { + throw new NotFoundException('User not found'); + } user.lastName = updateUserDto.lastName || user.lastName; user.firstName = updateUserDto.firstName || user.firstName; @@ -100,6 +110,7 @@ export class UserService { try { const updatedUser = await this.userRepository.save(user); + // TODO: this should probably retrun UserResponseDto return { id: updatedUser.id, email: updatedUser.email, @@ -133,6 +144,9 @@ export class UserService { async getUserProfileImage(userId: string, res: Res) { try { const user = await this.userRepository.findOne({ id: userId }); + if (!user) { + throw new NotFoundException('User not found'); + } if (!user.profileImagePath) { // throw new BadRequestException('User does not have a profile image'); diff --git a/server/apps/immich/src/app.controller.ts b/server/apps/immich/src/app.controller.ts index 76402ba233..b657baf8c6 100644 --- a/server/apps/immich/src/app.controller.ts +++ b/server/apps/immich/src/app.controller.ts @@ -1,6 +1,3 @@ -import { Controller, Get, Res, Headers } from '@nestjs/common'; -import { Response } from 'express'; +import { Controller } from '@nestjs/common'; @Controller() -export class AppController { - constructor() {} -} +export class AppController {} diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 88e8f606e6..a57964a417 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -4,8 +4,7 @@ import { AssetModule } from './api-v1/asset/asset.module'; import { AuthModule } from './api-v1/auth/auth.module'; import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module'; import { DeviceInfoModule } from './api-v1/device-info/device-info.module'; -import { AppLoggerMiddleware } from './middlewares/app-logger.middleware'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { immichAppConfig } from './config/app.config'; import { BullModule } from '@nestjs/bull'; import { ServerInfoModule } from './api-v1/server-info/server-info.module'; @@ -57,6 +56,8 @@ import { DatabaseModule } from '@app/database'; providers: [], }) 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 { if (process.env.NODE_ENV == 'development') { // consumer.apply(AppLoggerMiddleware).forRoutes('*'); diff --git a/server/apps/immich/src/config/asset-upload.config.ts b/server/apps/immich/src/config/asset-upload.config.ts index a868d0c356..cbed1b3576 100644 --- a/server/apps/immich/src/config/asset-upload.config.ts +++ b/server/apps/immich/src/config/asset-upload.config.ts @@ -6,7 +6,7 @@ import { extname } from 'path'; import { Request } from 'express'; import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant'; 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 = { fileFilter: (req: Request, file: any, cb: any) => { @@ -20,13 +20,18 @@ export const assetUploadOption: MulterOptions = { storage: diskStorage({ destination: (req: Request, file: Express.Multer.File, cb: any) => { 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 monthInfo = new Date(fileInfo.createdAt).getMonth(); + // const yearInfo = new Date(fileInfo.createdAt).getFullYear(); + // const monthInfo = new Date(fileInfo.createdAt).getMonth(); + + if (!req.user) { + return; + } 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)) { mkdirSync(originalUploadFolder, { recursive: true }); @@ -35,7 +40,7 @@ export const assetUploadOption: MulterOptions = { // Save original to disk cb(null, originalUploadFolder); } 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)) { mkdirSync(thumbnailUploadFolder, { recursive: true }); diff --git a/server/apps/immich/src/config/profile-image-upload.config.ts b/server/apps/immich/src/config/profile-image-upload.config.ts index 76e65e68c5..98ff0587ab 100644 --- a/server/apps/immich/src/config/profile-image-upload.config.ts +++ b/server/apps/immich/src/config/profile-image-upload.config.ts @@ -17,8 +17,11 @@ export const profileImageUploadOption: MulterOptions = { storage: diskStorage({ destination: (req: Request, file: Express.Multer.File, cb: any) => { + if (!req.user) { + return; + } const basePath = APP_UPLOAD_LOCATION; - const profileImageLocation = `${basePath}/${req.user['id']}/profile`; + const profileImageLocation = `${basePath}/${req.user.id}/profile`; if (!existsSync(profileImageLocation)) { mkdirSync(profileImageLocation, { recursive: true }); @@ -28,7 +31,10 @@ export const profileImageUploadOption: MulterOptions = { }, 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)}`); }, diff --git a/server/apps/immich/src/decorators/auth-user.decorator.ts b/server/apps/immich/src/decorators/auth-user.decorator.ts index 629fa62a8a..3ecd4257f7 100644 --- a/server/apps/immich/src/decorators/auth-user.decorator.ts +++ b/server/apps/immich/src/decorators/auth-user.decorator.ts @@ -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 { AuthUserDto } from './dto/auth-user.dto'; export class AuthUserDto { - id: string; - email: string; + id!: string; + email!: string; } 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(), email, }; diff --git a/server/apps/immich/src/global.d.ts b/server/apps/immich/src/global.d.ts new file mode 100644 index 0000000000..e37c9b5716 --- /dev/null +++ b/server/apps/immich/src/global.d.ts @@ -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 {} + } +} diff --git a/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts index 5719328e11..70739fec3a 100644 --- a/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts +++ b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts @@ -1,6 +1,5 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; 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 { userId } = await this.jwtService.validateToken(bearerToken); + if (!userId) { + return false; + } + const user = await this.userRepository.findOne(userId); + if (!user) { + return false; + } return user.isAdmin; } diff --git a/server/apps/immich/src/middlewares/app-logger.middleware.ts b/server/apps/immich/src/middlewares/app-logger.middleware.ts index b1e1d056dc..44accec190 100644 --- a/server/apps/immich/src/middlewares/app-logger.middleware.ts +++ b/server/apps/immich/src/middlewares/app-logger.middleware.ts @@ -7,7 +7,7 @@ export class AppLoggerMiddleware implements NestMiddleware { private logger = new Logger('HTTP'); 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') || ''; response.on('close', () => { diff --git a/server/apps/immich/src/modules/background-task/background-task.processor.ts b/server/apps/immich/src/modules/background-task/background-task.processor.ts index 900b5fffb6..5fa760c082 100644 --- a/server/apps/immich/src/modules/background-task/background-task.processor.ts +++ b/server/apps/immich/src/modules/background-task/background-task.processor.ts @@ -1,12 +1,10 @@ -import { InjectQueue, Process, Processor } from '@nestjs/bull'; +import { Process, Processor } from '@nestjs/bull'; import { InjectRepository } from '@nestjs/typeorm'; -import { Job, Queue } from 'bull'; import { Repository } from 'typeorm'; import { AssetEntity } from '@app/database/entities/asset.entity'; import fs from 'fs'; -import { Logger } from '@nestjs/common'; -import axios from 'axios'; import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; +import { Job } from 'bull'; @Processor('background-task') export class BackgroundTaskProcessor { @@ -18,9 +16,10 @@ export class BackgroundTaskProcessor { private smartInfoRepository: Repository, ) {} + // TODO: Should probably use constants / Interfaces for Queue names / data @Process('delete-file-on-disk') - async deleteFileOnDisk(job) { - const { assets }: { assets: AssetEntity[] } = job.data; + async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) { + const { assets } = job.data; for (const asset of assets) { fs.unlink(asset.originalPath, (err) => { @@ -29,11 +28,14 @@ export class BackgroundTaskProcessor { } }); - fs.unlink(asset.resizePath, (err) => { - if (err) { - console.log('error deleting ', asset.originalPath); - } - }); + // TODO: what if there is no asset.resizePath. Should fail the Job? + if (asset.resizePath) { + fs.unlink(asset.resizePath, (err) => { + if (err) { + console.log('error deleting ', asset.originalPath); + } + }); + } } } } diff --git a/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts index ba06fbceb4..f8e26d7c96 100644 --- a/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts +++ b/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts @@ -3,6 +3,11 @@ import { JwtService } from '@nestjs/jwt'; import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto'; import { jwtSecret } from '../../constants/jwt.constant'; +export type JwtValidationResult = { + status: boolean; + userId: string | null; +}; + @Injectable() export class ImmichJwtService { constructor(private jwtService: JwtService) {} @@ -13,11 +18,11 @@ export class ImmichJwtService { }); } - public async validateToken(accessToken: string) { + public async validateToken(accessToken: string): Promise { try { - const payload = await this.jwtService.verify(accessToken, { secret: jwtSecret }); + const payload = await this.jwtService.verifyAsync(accessToken, { secret: jwtSecret }); return { - userId: payload['userId'], + userId: payload.userId, status: true, }; } catch (e) { diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts index d085cb0398..bdf63482bd 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { ScheduleTasksService } from './schedule-tasks.service'; -import { MicroservicesModule } from '../../../../microservices/src/microservices.module'; @Module({ imports: [ diff --git a/server/apps/immich/test/user.e2e-spec.ts b/server/apps/immich/test/user.e2e-spec.ts index c12ad1d8a3..ab87fedcfb 100644 --- a/server/apps/immich/test/user.e2e-spec.ts +++ b/server/apps/immich/test/user.e2e-spec.ts @@ -8,7 +8,7 @@ import { UserModule } from '../src/api-v1/user/user.module'; import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module'; import { UserService } from '../src/api-v1/user/user.service'; 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) { return userService.createUser(data); @@ -44,7 +44,7 @@ describe('User', () => { describe('with auth', () => { let userService: UserService; - let authUser: User; + let authUser: UserResponseDto; beforeAll(async () => { const builder = Test.createTestingModule({ diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts index b7258fa3f5..ffa0f6c03e 100644 --- a/server/apps/microservices/src/microservices.module.ts +++ b/server/apps/microservices/src/microservices.module.ts @@ -11,8 +11,6 @@ import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; import { MetadataExtractionProcessor } from './processors/metadata-extraction.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'; @Module({ diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts index f616b83222..28ee924abb 100644 --- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -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 { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; import { InjectRepository } from '@nestjs/typeorm'; diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index 0a8a205d4b..07f47db334 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -11,13 +11,12 @@ import { readFile } from 'fs/promises'; import { Logger } from '@nestjs/common'; import axios from 'axios'; import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; -import { ConfigService } from '@nestjs/config'; import ffmpeg from 'fluent-ffmpeg'; // import moment from 'moment'; @Processor('metadata-extraction-queue') export class MetadataExtractionProcessor { - private geocodingClient: GeocodeService; + private geocodingClient?: GeocodeService; constructor( @InjectRepository(AssetEntity) @@ -29,7 +28,7 @@ export class MetadataExtractionProcessor { @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository, ) { - if (process.env.ENABLE_MAPBOX == 'true') { + if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) { this.geocodingClient = mapboxGeocoding({ accessToken: process.env.MAPBOX_KEY, }); @@ -65,7 +64,7 @@ export class MetadataExtractionProcessor { newExif.longitude = exifData['longitude'] || null; // Reverse GeoCoding - if (process.env.ENABLE_MAPBOX && exifData['longitude'] && exifData['latitude']) { + if (this.geocodingClient && exifData['longitude'] && exifData['latitude']) { const geoCodeInfo: MapiResponse = await this.geocodingClient .reverseGeocode({ query: [exifData['longitude'], exifData['latitude']], @@ -86,7 +85,7 @@ export class MetadataExtractionProcessor { await this.exifRepository.save(newExif); } 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) { - Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`); + Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); } } diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts index eae53034ca..c077363379 100644 --- a/server/apps/microservices/src/processors/thumbnail.processor.ts +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -43,7 +43,7 @@ export class ThumbnailGeneratorProcessor { sharp(asset.originalPath) .resize(1440, 2560, { fit: 'inside' }) .jpeg() - .toFile(jpegThumbnailPath, async (err, info) => { + .toFile(jpegThumbnailPath, async (err) => { if (!err) { await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath }); @@ -65,7 +65,7 @@ export class ThumbnailGeneratorProcessor { .on('start', () => { Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail'); }) - .on('error', (error, b, c) => { + .on('error', (error) => { Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail'); // reject(); }) @@ -87,15 +87,18 @@ export class ThumbnailGeneratorProcessor { } @Process({ name: 'generate-webp-thumbnail', concurrency: 2 }) - async generateWepbThumbnail(job: Job) { - const { asset }: { asset: AssetEntity } = job.data; + async generateWepbThumbnail(job: Job<{ asset: AssetEntity }>) { + const { asset } = job.data; + if (!asset.resizePath) { + return; + } const webpPath = asset.resizePath.replace('jpeg', 'webp'); sharp(asset.resizePath) .resize(250) .webp() - .toFile(webpPath, (err, info) => { + .toFile(webpPath, (err) => { if (!err) { this.assetRepository.update({ id: asset.id }, { webpPath: webpPath }); } diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index 172dfbb7b2..984124a7db 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -44,7 +44,7 @@ export class VideoTranscodeProcessor { .on('start', () => { Logger.log('Start Converting Video', 'mp4Conversion'); }) - .on('error', (error, b, c) => { + .on('error', (error) => { Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion'); reject(); }) diff --git a/server/apps/microservices/test/app.e2e-spec.ts b/server/apps/microservices/test/app.e2e-spec.ts index edd3fc48cf..36cf0affb9 100644 --- a/server/apps/microservices/test/app.e2e-spec.ts +++ b/server/apps/microservices/test/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { MicroservicesModule } from './../src/microservices.module'; describe('MicroservicesController (e2e)', () => { diff --git a/server/libs/database/src/entities/album.entity.ts b/server/libs/database/src/entities/album.entity.ts index 8de60a3b39..55dbe3a83a 100644 --- a/server/libs/database/src/entities/album.entity.ts +++ b/server/libs/database/src/entities/album.entity.ts @@ -5,23 +5,23 @@ import { UserAlbumEntity } from './user-album.entity'; @Entity('albums') export class AlbumEntity { @PrimaryGeneratedColumn('uuid') - id: string; + id!: string; @Column() - ownerId: string; + ownerId!: string; @Column({ default: 'Untitled Album' }) - albumName: string; + albumName!: string; @CreateDateColumn({ type: 'timestamptz' }) - createdAt: string; + createdAt!: string; - @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true }) - albumThumbnailAssetId: string; + @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true}) + albumThumbnailAssetId!: string | null; @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo) - sharedUsers: UserAlbumEntity[]; + sharedUsers?: UserAlbumEntity[]; @OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo) - assets: AssetAlbumEntity[]; + assets?: AssetAlbumEntity[]; } diff --git a/server/libs/database/src/entities/asset-album.entity.ts b/server/libs/database/src/entities/asset-album.entity.ts index de76de55de..b803b418ff 100644 --- a/server/libs/database/src/entities/asset-album.entity.ts +++ b/server/libs/database/src/entities/asset-album.entity.ts @@ -6,25 +6,25 @@ import { AssetEntity } from './asset.entity'; @Unique('PK_unique_asset_in_album', ['albumId', 'assetId']) export class AssetAlbumEntity { @PrimaryGeneratedColumn() - id: string; + id!: string; @Column() - albumId: string; + albumId!: string; @Column() - assetId: string; + assetId!: string; @ManyToOne(() => AlbumEntity, (album) => album.assets, { onDelete: 'CASCADE', nullable: true, }) @JoinColumn({ name: 'albumId' }) - albumInfo: AlbumEntity; + albumInfo!: AlbumEntity; @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true, }) @JoinColumn({ name: 'assetId' }) - assetInfo: AssetEntity; + assetInfo!: AssetEntity; } diff --git a/server/libs/database/src/entities/asset.entity.ts b/server/libs/database/src/entities/asset.entity.ts index 557187e58d..618befcdb6 100644 --- a/server/libs/database/src/entities/asset.entity.ts +++ b/server/libs/database/src/entities/asset.entity.ts @@ -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 { SmartInfoEntity } from './smart-info.entity'; @@ -6,52 +6,52 @@ import { SmartInfoEntity } from './smart-info.entity'; @Unique(['deviceAssetId', 'userId', 'deviceId']) export class AssetEntity { @PrimaryGeneratedColumn('uuid') - id: string; + id!: string; @Column() - deviceAssetId: string; + deviceAssetId!: string; @Column() - userId: string; + userId!: string; @Column() - deviceId: string; + deviceId!: string; @Column() - type: AssetType; + type!: AssetType; @Column() - originalPath: string; + originalPath!: string; - @Column({ nullable: true }) - resizePath: string; + @Column({ type: 'varchar', nullable: true }) + resizePath!: string | null; - @Column({ nullable: true }) - webpPath: string; + @Column({ type: 'varchar', nullable: true }) + webpPath!: string | null; - @Column({ nullable: true }) - encodedVideoPath: string; + @Column({ type: 'varchar', nullable: true }) + encodedVideoPath!: string; @Column() - createdAt: string; + createdAt!: string; @Column() - modifiedAt: string; + modifiedAt!: string; @Column({ type: 'boolean', default: false }) - isFavorite: boolean; + isFavorite!: boolean; - @Column({ nullable: true }) - mimeType: string; + @Column({ type: 'varchar', nullable: true }) + mimeType!: string | null; - @Column({ nullable: true }) - duration: string; + @Column({ type: 'varchar', nullable: true }) + duration!: string | null; @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset) - exifInfo: ExifEntity; + exifInfo?: ExifEntity; @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset) - smartInfo: SmartInfoEntity; + smartInfo?: SmartInfoEntity; } export enum AssetType { diff --git a/server/libs/database/src/entities/device-info.entity.ts b/server/libs/database/src/entities/device-info.entity.ts index e7d773fd9e..3ca251387f 100644 --- a/server/libs/database/src/entities/device-info.entity.ts +++ b/server/libs/database/src/entities/device-info.entity.ts @@ -4,25 +4,25 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from @Unique(['userId', 'deviceId']) export class DeviceInfoEntity { @PrimaryGeneratedColumn() - id: number; + id!: number; @Column() - userId: string; + userId!: string; @Column() - deviceId: string; + deviceId!: string; @Column() - deviceType: DeviceType; + deviceType!: DeviceType; - @Column({ nullable: true }) - notificationToken: string; + @Column({ type: 'varchar', nullable: true }) + notificationToken!: string | null; @CreateDateColumn() - createdAt: string; + createdAt!: string; @Column({ type: 'bool', default: false }) - isAutoBackup: boolean; + isAutoBackup!: boolean; } export enum DeviceType { diff --git a/server/libs/database/src/entities/exif.entity.ts b/server/libs/database/src/entities/exif.entity.ts index f44002640c..fa9d8f8000 100644 --- a/server/libs/database/src/entities/exif.entity.ts +++ b/server/libs/database/src/entities/exif.entity.ts @@ -7,70 +7,70 @@ import { AssetEntity } from './asset.entity'; @Entity('exif') export class ExifEntity { @PrimaryGeneratedColumn() - id: string; + id!: string; @Index({ unique: true }) @Column({ type: 'uuid' }) - assetId: string; + assetId!: string; - @Column({ nullable: true }) - make: string; + @Column({ type: 'varchar', nullable: true }) + make!: string | null; - @Column({ nullable: true }) - model: string; + @Column({ type: 'varchar', nullable: true }) + model!: string | null; - @Column({ nullable: true }) - imageName: string; + @Column({ type: 'varchar', nullable: true }) + imageName!: string | null; - @Column({ nullable: true }) - exifImageWidth: number; + @Column({ type: 'integer', nullable: true }) + exifImageWidth!: number | null; - @Column({ nullable: true }) - exifImageHeight: number; + @Column({ type: 'integer', nullable: true }) + exifImageHeight!: number | null; - @Column({ nullable: true }) - fileSizeInByte: number; + @Column({ type: 'integer', nullable: true }) + fileSizeInByte!: number | null; - @Column({ nullable: true }) - orientation: string; + @Column({ type: 'varchar', nullable: true }) + orientation!: string | null; @Column({ type: 'timestamptz', nullable: true }) - dateTimeOriginal: Date; + dateTimeOriginal!: Date | null; @Column({ type: 'timestamptz', nullable: true }) - modifyDate: Date; + modifyDate!: Date | null; - @Column({ nullable: true }) - lensModel: string; + @Column({ type: 'varchar', nullable: true }) + lensModel!: string | null; @Column({ type: 'float8', nullable: true }) - fNumber: number; + fNumber!: number | null; @Column({ type: 'float8', nullable: true }) - focalLength: number; + focalLength!: number | null; - @Column({ nullable: true }) - iso: number; + @Column({ type: 'integer', nullable: true }) + iso!: number | null; @Column({ type: 'float', nullable: true }) - exposureTime: number; + exposureTime!: number | null; @Column({ type: 'float', nullable: true }) - latitude: number; + latitude!: number | null; @Column({ type: 'float', nullable: true }) - longitude: number; + longitude!: number | null; - @Column({ nullable: true }) - city: string; + @Column({ type: 'varchar', nullable: true }) + city!: string | null; - @Column({ nullable: true }) - state: string; + @Column({ type: 'varchar', nullable: true }) + state!: string | null; - @Column({ nullable: true }) - country: string; + @Column({ type: 'varchar', nullable: true }) + country!: string | null; @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) - asset: ExifEntity; + asset?: ExifEntity; } diff --git a/server/libs/database/src/entities/smart-info.entity.ts b/server/libs/database/src/entities/smart-info.entity.ts index cccbd56773..f1b46e5cee 100644 --- a/server/libs/database/src/entities/smart-info.entity.ts +++ b/server/libs/database/src/entities/smart-info.entity.ts @@ -4,19 +4,19 @@ import { AssetEntity } from './asset.entity'; @Entity('smart_info') export class SmartInfoEntity { @PrimaryGeneratedColumn() - id: string; + id!: string; @Index({ unique: true }) @Column({ type: 'uuid' }) - assetId: string; + assetId!: string; @Column({ type: 'text', array: true, nullable: true }) - tags: string[]; + tags!: string[] | null; @Column({ type: 'text', array: true, nullable: true }) - objects: string[]; + objects!: string[] | null; @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) - asset: SmartInfoEntity; + asset?: SmartInfoEntity; } diff --git a/server/libs/database/src/entities/user-album.entity.ts b/server/libs/database/src/entities/user-album.entity.ts index f910904a87..fe08ff5a86 100644 --- a/server/libs/database/src/entities/user-album.entity.ts +++ b/server/libs/database/src/entities/user-album.entity.ts @@ -6,22 +6,22 @@ import { AlbumEntity } from './album.entity'; @Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId']) export class UserAlbumEntity { @PrimaryGeneratedColumn() - id: string; + id!: string; @Column() - albumId: string; + albumId!: string; @Column() - sharedUserId: string; + sharedUserId!: string; @ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, { onDelete: 'CASCADE', nullable: true, }) @JoinColumn({ name: 'albumId' }) - albumInfo: AlbumEntity; + albumInfo!: AlbumEntity; @ManyToOne(() => UserEntity) @JoinColumn({ name: 'sharedUserId' }) - userInfo: UserEntity; + userInfo!: UserEntity; } diff --git a/server/libs/database/src/entities/user.entity.ts b/server/libs/database/src/entities/user.entity.ts index e3ad95bb7c..c9ae1e2bb0 100644 --- a/server/libs/database/src/entities/user.entity.ts +++ b/server/libs/database/src/entities/user.entity.ts @@ -3,32 +3,32 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeor @Entity('users') export class UserEntity { @PrimaryGeneratedColumn('uuid') - id: string; + id!: string; @Column() - firstName: string; + firstName!: string; @Column() - lastName: string; + lastName!: string; @Column() - isAdmin: boolean; + isAdmin!: boolean; @Column() - email: string; + email!: string; @Column({ select: false }) - password: string; + password?: string; @Column({ select: false }) - salt: string; + salt?: string; @Column() - profileImagePath: string; + profileImagePath!: string; @Column() - isFirstLoggedIn: boolean; + isFirstLoggedIn!: boolean; @CreateDateColumn() - createdAt: string; + createdAt!: string; } diff --git a/server/package-lock.json b/server/package-lock.json index fe03142ab6..d563f8d2e1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -59,6 +59,7 @@ "@types/imagemin": "^8.0.0", "@types/jest": "27.0.2", "@types/lodash": "^4.14.178", + "@types/mapbox__mapbox-sdk": "^0.13.4", "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/passport-jwt": "^3.0.6", @@ -2205,6 +2206,12 @@ "@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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2312,6 +2319,26 @@ "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -12822,6 +12849,12 @@ "@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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -12929,6 +12962,26 @@ "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", diff --git a/server/package.json b/server/package.json index 6333be06ee..81b605ebdb 100644 --- a/server/package.json +++ b/server/package.json @@ -13,7 +13,10 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "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:watch": "jest --watch", "test:cov": "jest --coverage", @@ -72,6 +75,7 @@ "@types/imagemin": "^8.0.0", "@types/jest": "27.0.2", "@types/lodash": "^4.14.178", + "@types/mapbox__mapbox-sdk": "^0.13.4", "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/passport-jwt": "^3.0.6", diff --git a/server/tsconfig.json b/server/tsconfig.json index e7e31bffa6..6f892ac09e 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "strict": true, "declaration": true, "removeComments": true, "emitDecoratorMetadata": true,