1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +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:
Jaime Baez 2022-06-25 19:53:06 +02:00 committed by GitHub
parent cca2f7d178
commit c918f5b001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 415 additions and 273 deletions

View file

@ -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> {

View file

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

View file

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

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class AddAssetsDto { export class AddAssetsDto {
@IsNotEmpty() @IsNotEmpty()
assetIds: string[]; assetIds!: string[];
} }

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class AddUsersDto { export class AddUsersDto {
@IsNotEmpty() @IsNotEmpty()
sharedUserIds: string[]; sharedUserIds!: string[];
} }

View file

@ -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[];

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class RemoveAssetsDto { export class RemoveAssetsDto {
@IsNotEmpty() @IsNotEmpty()
assetIds: string[]; assetIds!: string[];
} }

View file

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

View file

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

View file

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

View file

@ -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());

View file

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

View file

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

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class DeleteAssetDto { export class DeleteAssetDto {
@IsNotEmpty() @IsNotEmpty()
ids: string[]; ids!: string[];
} }

View file

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

View file

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

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class GetAssetDto { export class GetAssetDto {
@IsNotEmpty() @IsNotEmpty()
deviceId: string; deviceId!: string;
} }

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class GetNewAssetQueryDto { export class GetNewAssetQueryDto {
@IsNotEmpty() @IsNotEmpty()
latestDate: string; latestDate!: string;
} }

View file

@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
export class SearchAssetDto { export class SearchAssetDto {
@IsNotEmpty() @IsNotEmpty()
searchTerm: string; searchTerm!: string;
} }

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

@ -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) {}

View file

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

View file

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

View file

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

View file

@ -3,9 +3,7 @@ import {
Get, Get,
Post, Post,
Body, Body,
Patch,
Param, Param,
Delete,
UseGuards, UseGuards,
ValidationPipe, ValidationPipe,
Put, Put,

View file

@ -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');

View file

@ -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() {}
}

View file

@ -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('*');

View file

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

View file

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

View file

@ -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
View 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 {}
}
}

View file

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

View file

@ -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', () => {

View file

@ -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,6 +28,8 @@ export class BackgroundTaskProcessor {
} }
}); });
// TODO: what if there is no asset.resizePath. Should fail the Job?
if (asset.resizePath) {
fs.unlink(asset.resizePath, (err) => { fs.unlink(asset.resizePath, (err) => {
if (err) { if (err) {
console.log('error deleting ', asset.originalPath); console.log('error deleting ', asset.originalPath);
@ -37,3 +38,4 @@ export class BackgroundTaskProcessor {
} }
} }
} }
}

View file

@ -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) {

View file

@ -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: [

View file

@ -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({

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

@ -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)', () => {

View file

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

View file

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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

@ -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",

View file

@ -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",

View file

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"strict": true,
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,