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