You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-09 23:17:29 +02:00
Add OpenAPI Specs and Response DTOs (#320)
* Added swagger bearer auth method authentication accordingly * Update Auth endpoint * Added additional api information for authentication * Added Swagger CLI pluggin * Added DTO for /user endpoint * Added /device-info reponse DTOs * Implement server version * Added DTOs for /server-info * Added DTOs for /assets * Added album to Swagger group * Added generated specs file * Add Client API generator for web * Remove incorrectly placed node_modules * Created class to handle access token * Remove password and hash when getting all user * PR feedback * Fixed video from CLI doesn't get metadata extracted * Fixed issue with TSConfig to work with generated openAPI * PR feedback * Remove console.log
This commit is contained in:
@@ -22,20 +22,23 @@ import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Album')
|
||||
@Controller('album')
|
||||
export class AlbumController {
|
||||
constructor(private readonly albumService: AlbumService) {}
|
||||
|
||||
@Post()
|
||||
async create(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
|
||||
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
|
||||
return this.albumService.create(authUser, createAlbumDto);
|
||||
}
|
||||
|
||||
@Put('/:albumId/users')
|
||||
async addUsers(
|
||||
async addUsersToAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) addUsersDto: AddUsersDto,
|
||||
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
|
||||
@@ -44,7 +47,7 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Put('/:albumId/assets')
|
||||
async addAssets(
|
||||
async addAssetsToAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) addAssetsDto: AddAssetsDto,
|
||||
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
|
||||
|
@@ -2,15 +2,15 @@ import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.
|
||||
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
|
||||
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
|
||||
|
||||
export interface AlbumResponseDto {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
albumName: string;
|
||||
createdAt: string;
|
||||
albumThumbnailAssetId: string | null;
|
||||
shared: boolean;
|
||||
sharedUsers: UserResponseDto[];
|
||||
assets: AssetResponseDto[];
|
||||
export class AlbumResponseDto {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
albumName!: string;
|
||||
createdAt!: string;
|
||||
albumThumbnailAssetId!: string | null;
|
||||
shared!: boolean;
|
||||
sharedUsers!: UserResponseDto[];
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||
|
@@ -36,8 +36,14 @@ import { IAssetUploadedJob } from '@app/job/index';
|
||||
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
||||
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||
import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||
import { AssetResponseDto } from './response-dto/asset-response.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Asset')
|
||||
@Controller('asset')
|
||||
export class AssetController {
|
||||
constructor(
|
||||
@@ -89,7 +95,7 @@ export class AssetController {
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Query(ValidationPipe) query: ServeFileDto,
|
||||
) {
|
||||
): Promise<StreamableFile> {
|
||||
return this.assetService.downloadFile(query, res);
|
||||
}
|
||||
|
||||
@@ -109,43 +115,58 @@ export class AssetController {
|
||||
}
|
||||
|
||||
@Get('/allObjects')
|
||||
async getCuratedObject(@GetAuthUser() authUser: AuthUserDto) {
|
||||
async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
|
||||
return this.assetService.getCuratedObject(authUser);
|
||||
}
|
||||
|
||||
@Get('/allLocation')
|
||||
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
|
||||
async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
|
||||
return this.assetService.getCuratedLocation(authUser);
|
||||
}
|
||||
|
||||
@Get('/searchTerm')
|
||||
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
|
||||
async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<String[]> {
|
||||
return this.assetService.getAssetSearchTerm(authUser);
|
||||
}
|
||||
|
||||
@Post('/search')
|
||||
async searchAsset(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) searchAssetDto: SearchAssetDto) {
|
||||
async searchAsset(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) searchAssetDto: SearchAssetDto,
|
||||
): Promise<AssetResponseDto[]> {
|
||||
return this.assetService.searchAsset(authUser, searchAssetDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@Get('/')
|
||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
|
||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> {
|
||||
return await this.assetService.getAllAssets(authUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*/
|
||||
@Get('/:deviceId')
|
||||
async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
|
||||
return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single asset's information
|
||||
*/
|
||||
@Get('/assetById/:assetId')
|
||||
async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId: string) {
|
||||
async getAssetById(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Param('assetId') assetId: string,
|
||||
): Promise<AssetResponseDto> {
|
||||
return await this.assetService.getAssetById(authUser, assetId);
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
async deleteAssetById(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
|
||||
const deleteAssetList: AssetEntity[] = [];
|
||||
async deleteAsset(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
|
||||
const deleteAssetList: AssetResponseDto[] = [];
|
||||
|
||||
for (const id of assetIds.ids) {
|
||||
const assets = await this.assetService.getAssetById(authUser, id);
|
||||
|
@@ -19,6 +19,8 @@ import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||
import fs from 'fs/promises';
|
||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@@ -80,49 +82,55 @@ export class AssetService {
|
||||
return res;
|
||||
}
|
||||
|
||||
public async getAllAssets(authUser: AuthUserDto) {
|
||||
try {
|
||||
return await this.assetRepository.find({
|
||||
where: {
|
||||
userId: authUser.id,
|
||||
resizePath: Not(IsNull()),
|
||||
},
|
||||
relations: ['exifInfo'],
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.error(e, 'getAllAssets');
|
||||
}
|
||||
public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
|
||||
const assets = await this.assetRepository.find({
|
||||
where: {
|
||||
userId: authUser.id,
|
||||
resizePath: Not(IsNull()),
|
||||
},
|
||||
relations: ['exifInfo'],
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
return assets.map((asset) => mapAsset(asset));
|
||||
}
|
||||
|
||||
public async findOne(deviceId: string, assetId: string): Promise<AssetEntity> {
|
||||
public async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
|
||||
const rows = await this.assetRepository.query(
|
||||
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2',
|
||||
[assetId, deviceId],
|
||||
);
|
||||
|
||||
if (rows.lengh == 0) {
|
||||
throw new BadRequestException('Not Found');
|
||||
throw new NotFoundException('Not Found');
|
||||
}
|
||||
|
||||
return rows[0] as AssetEntity;
|
||||
const assetOnDevice = rows[0] as AssetEntity;
|
||||
|
||||
return mapAsset(assetOnDevice);
|
||||
}
|
||||
|
||||
public async getAssetById(authUser: AuthUserDto, assetId: string) {
|
||||
return await this.assetRepository.findOne({
|
||||
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
|
||||
const asset = await this.assetRepository.findOne({
|
||||
where: {
|
||||
id: assetId,
|
||||
},
|
||||
relations: ['exifInfo'],
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset not found');
|
||||
}
|
||||
|
||||
return mapAsset(asset);
|
||||
}
|
||||
|
||||
public async downloadFile(query: ServeFileDto, res: Res) {
|
||||
try {
|
||||
let fileReadStream = null;
|
||||
const asset = await this.findOne(query.did, query.aid);
|
||||
const asset = await this.findAssetOfDevice(query.did, query.aid);
|
||||
|
||||
if (query.isThumb === 'false' || !query.isThumb) {
|
||||
const { size } = await fileInfo(asset.originalPath);
|
||||
@@ -188,7 +196,7 @@ export class AssetService {
|
||||
|
||||
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
||||
let fileReadStream: ReadStream;
|
||||
const asset = await this.findOne(query.did, query.aid);
|
||||
const asset = await this.findAssetOfDevice(query.did, query.aid);
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset does not exist');
|
||||
@@ -258,12 +266,13 @@ export class AssetService {
|
||||
try {
|
||||
// Handle Video
|
||||
let videoPath = asset.originalPath;
|
||||
|
||||
let mimeType = asset.mimeType;
|
||||
|
||||
await fs.access(videoPath, constants.R_OK | constants.W_OK);
|
||||
|
||||
if (query.isWeb && asset.mimeType == 'video/quicktime') {
|
||||
videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
|
||||
videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath);
|
||||
mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
|
||||
}
|
||||
|
||||
@@ -390,7 +399,7 @@ export class AssetService {
|
||||
return Array.from(possibleSearchTerm).filter((x) => x != null);
|
||||
}
|
||||
|
||||
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto) {
|
||||
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> {
|
||||
const query = `
|
||||
SELECT a.*
|
||||
FROM assets a
|
||||
@@ -406,7 +415,12 @@ export class AssetService {
|
||||
);
|
||||
`;
|
||||
|
||||
return await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
|
||||
const searchResults: AssetEntity[] = await this.assetRepository.query(query, [
|
||||
authUser.id,
|
||||
searchAssetDto.searchTerm,
|
||||
]);
|
||||
|
||||
return searchResults.map((asset) => mapAsset(asset));
|
||||
}
|
||||
|
||||
async getCuratedLocation(authUser: AuthUserDto) {
|
||||
@@ -423,8 +437,8 @@ export class AssetService {
|
||||
);
|
||||
}
|
||||
|
||||
async getCuratedObject(authUser: AuthUserDto) {
|
||||
return await this.assetRepository.query(
|
||||
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
|
||||
const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query(
|
||||
`
|
||||
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
|
||||
FROM assets a
|
||||
@@ -434,9 +448,11 @@ export class AssetService {
|
||||
`,
|
||||
[authUser.id],
|
||||
);
|
||||
|
||||
return curatedObjects;
|
||||
}
|
||||
|
||||
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto) {
|
||||
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto): Promise<boolean> {
|
||||
const res = await this.assetRepository.findOne({
|
||||
where: {
|
||||
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
|
||||
|
@@ -1,6 +1,17 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class DeleteAssetDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: String,
|
||||
title: 'Array of asset IDs to delete',
|
||||
example: [
|
||||
'bf973405-3f2a-48d2-a687-2ed4167164be',
|
||||
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
|
||||
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
|
||||
],
|
||||
})
|
||||
ids!: string[];
|
||||
}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class ServeFileDto {
|
||||
//assetId
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ type: String, title: 'Device Asset ID' })
|
||||
aid!: string;
|
||||
|
||||
//deviceId
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ type: String, title: 'Device ID' })
|
||||
did!: string;
|
||||
|
||||
@IsOptional()
|
||||
|
@@ -2,19 +2,21 @@ import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
||||
|
||||
export interface AssetResponseDto {
|
||||
id: string;
|
||||
deviceAssetId: string;
|
||||
ownerId: string;
|
||||
deviceId: string;
|
||||
type: AssetType;
|
||||
originalPath: string;
|
||||
resizePath: string | null;
|
||||
createdAt: string;
|
||||
modifiedAt: string;
|
||||
isFavorite: boolean;
|
||||
mimeType: string | null;
|
||||
duration: string;
|
||||
export class AssetResponseDto {
|
||||
id!: string;
|
||||
deviceAssetId!: string;
|
||||
ownerId!: string;
|
||||
deviceId!: string;
|
||||
type!: AssetType;
|
||||
originalPath!: string;
|
||||
resizePath!: string | null;
|
||||
createdAt!: string;
|
||||
modifiedAt!: string;
|
||||
isFavorite!: boolean;
|
||||
mimeType!: string | null;
|
||||
duration!: string;
|
||||
webpPath!: string | null;
|
||||
encodedVideoPath!: string | null;
|
||||
exifInfo?: ExifResponseDto;
|
||||
smartInfo?: SmartInfoResponseDto;
|
||||
}
|
||||
@@ -32,6 +34,8 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
||||
modifiedAt: entity.modifiedAt,
|
||||
isFavorite: entity.isFavorite,
|
||||
mimeType: entity.mimeType,
|
||||
webpPath: entity.webpPath,
|
||||
encodedVideoPath: entity.encodedVideoPath,
|
||||
duration: entity.duration ?? '0:00:00.00000',
|
||||
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
||||
|
@@ -0,0 +1,7 @@
|
||||
export class CuratedLocationsResponseDto {
|
||||
id!: string;
|
||||
city!: string;
|
||||
resizePath!: string;
|
||||
deviceAssetId!: string;
|
||||
deviceId!: string;
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
export class CuratedObjectsResponseDto {
|
||||
id!: string;
|
||||
object!: string;
|
||||
resizePath!: string;
|
||||
deviceAssetId!: string;
|
||||
deviceId!: string;
|
||||
}
|
@@ -1,26 +1,26 @@
|
||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||
|
||||
export interface ExifResponseDto {
|
||||
id: string;
|
||||
make: string | null;
|
||||
model: string | null;
|
||||
imageName: string | null;
|
||||
exifImageWidth: number | null;
|
||||
exifImageHeight: number | null;
|
||||
fileSizeInByte: number | null;
|
||||
orientation: string | null;
|
||||
dateTimeOriginal: Date | null;
|
||||
modifyDate: Date | null;
|
||||
lensModel: string | null;
|
||||
fNumber: number | null;
|
||||
focalLength: number | null;
|
||||
iso: number | null;
|
||||
exposureTime: number | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
country: string | null;
|
||||
export class ExifResponseDto {
|
||||
id!: string;
|
||||
make: string | null = null;
|
||||
model: string | null = null;
|
||||
imageName: string | null = null;
|
||||
exifImageWidth: number | null = null;
|
||||
exifImageHeight: number | null = null;
|
||||
fileSizeInByte: number | null = null;
|
||||
orientation: string | null = null;
|
||||
dateTimeOriginal: Date | null = null;
|
||||
modifyDate: Date | null = null;
|
||||
lensModel: string | null = null;
|
||||
fNumber: number | null = null;
|
||||
focalLength: number | null = null;
|
||||
iso: number | null = null;
|
||||
exposureTime: number | null = null;
|
||||
latitude: number | null = null;
|
||||
longitude: number | null = null;
|
||||
city: string | null = null;
|
||||
state: string | null = null;
|
||||
country: string | null = null;
|
||||
}
|
||||
|
||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||
|
||||
export interface SmartInfoResponseDto {
|
||||
id: string;
|
||||
tags: string[] | null;
|
||||
objects: string[] | null;
|
||||
export class SmartInfoResponseDto {
|
||||
id?: string;
|
||||
tags?: string[] | null;
|
||||
objects?: string[] | null;
|
||||
}
|
||||
|
||||
export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
|
||||
|
@@ -1,30 +1,42 @@
|
||||
import { Body, Controller, Post, UseGuards, ValidationPipe } from '@nestjs/common';
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiCreatedResponse,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginCredentialDto } from './dto/login-credential.dto';
|
||||
import { LoginResponseDto } from './response-dto/login-response.dto';
|
||||
import { SignUpDto } from './dto/sign-up.dto';
|
||||
import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
|
||||
import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('/login')
|
||||
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
||||
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
|
||||
return await this.authService.login(loginCredential);
|
||||
}
|
||||
|
||||
@Post('/admin-sign-up')
|
||||
async adminSignUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
||||
return await this.authService.adminSignUp(signUpCrendential);
|
||||
@ApiBadRequestResponse({ description: 'The server already has an admin' })
|
||||
async adminSignUp(@Body(ValidationPipe) signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
|
||||
return await this.authService.adminSignUp(signUpCredential);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Post('/validateToken')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async validateToken(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return {
|
||||
authStatus: true,
|
||||
};
|
||||
async validateAccessToken(@GetAuthUser() authUser: AuthUserDto): Promise<ValidateAccessTokenResponseDto> {
|
||||
return new ValidateAccessTokenResponseDto(true);
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||
import { JwtPayloadDto } from './dto/jwt-payload.dto';
|
||||
import { SignUpDto } from './dto/sign-up.dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { mapUser, UserResponseDto } from '../user/response-dto/user-response.dto';
|
||||
import { LoginResponseDto, mapLoginResponse } from './response-dto/login-response.dto';
|
||||
import { AdminSignupResponseDto, mapAdminSignupResponse } from './response-dto/admin-signup-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -49,7 +50,7 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async login(loginCredential: LoginCredentialDto) {
|
||||
public async login(loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
|
||||
const validatedUser = await this.validateUser(loginCredential);
|
||||
|
||||
if (!validatedUser) {
|
||||
@@ -57,20 +58,12 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const payload = new JwtPayloadDto(validatedUser.id, validatedUser.email);
|
||||
const accessToken = await this.immichJwtService.generateToken(payload);
|
||||
|
||||
return {
|
||||
accessToken: await this.immichJwtService.generateToken(payload),
|
||||
userId: validatedUser.id,
|
||||
userEmail: validatedUser.email,
|
||||
firstName: validatedUser.firstName,
|
||||
lastName: validatedUser.lastName,
|
||||
isAdmin: validatedUser.isAdmin,
|
||||
profileImagePath: validatedUser.profileImagePath,
|
||||
shouldChangePassword: validatedUser.shouldChangePassword,
|
||||
};
|
||||
return mapLoginResponse(validatedUser, accessToken);
|
||||
}
|
||||
|
||||
public async adminSignUp(signUpCredential: SignUpDto): Promise<UserResponseDto> {
|
||||
public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
|
||||
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
|
||||
|
||||
if (adminUser) {
|
||||
@@ -88,7 +81,7 @@ export class AuthService {
|
||||
try {
|
||||
const savedNewAdminUserUser = await this.userRepository.save(newAdminUser);
|
||||
|
||||
return mapUser(savedNewAdminUserUser);
|
||||
return mapAdminSignupResponse(savedNewAdminUserUser);
|
||||
} catch (e) {
|
||||
Logger.error('e', 'signUp');
|
||||
throw new InternalServerErrorException('Failed to register new admin user');
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class LoginCredentialDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'testuser@email.com' })
|
||||
email!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'password' })
|
||||
password!: string;
|
||||
}
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class SignUpDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'testuser@email.com' })
|
||||
email!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'password' })
|
||||
password!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Admin' })
|
||||
firstName!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Doe' })
|
||||
lastName!: string;
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
|
||||
export class AdminSignupResponseDto {
|
||||
id!: string;
|
||||
email!: string;
|
||||
firstName!: string;
|
||||
lastName!: string;
|
||||
createdAt!: string;
|
||||
}
|
||||
|
||||
export function mapAdminSignupResponse(entity: UserEntity): AdminSignupResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
email: entity.email,
|
||||
firstName: entity.firstName,
|
||||
lastName: entity.lastName,
|
||||
createdAt: entity.createdAt,
|
||||
};
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginResponseDto {
|
||||
@ApiResponseProperty()
|
||||
accessToken!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
userId!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
userEmail!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
firstName!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
lastName!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
profileImagePath!: string;
|
||||
|
||||
@ApiResponseProperty()
|
||||
isAdmin!: boolean;
|
||||
|
||||
@ApiResponseProperty()
|
||||
shouldChangePassword!: boolean;
|
||||
}
|
||||
|
||||
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
userId: entity.id,
|
||||
userEmail: entity.email,
|
||||
firstName: entity.firstName,
|
||||
lastName: entity.lastName,
|
||||
isAdmin: entity.isAdmin,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
shouldChangePassword: entity.shouldChangePassword,
|
||||
};
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
export class ValidateAccessTokenResponseDto {
|
||||
constructor(authStatus: boolean) {
|
||||
this.authStatus = authStatus;
|
||||
}
|
||||
|
||||
authStatus: boolean;
|
||||
}
|
@@ -1,22 +1,32 @@
|
||||
import { Controller, Post, Body, Patch, UseGuards, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { DeviceInfoService } from './device-info.service';
|
||||
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
|
||||
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
|
||||
import { DeviceInfoResponseDto } from './response-dto/create-device-info-response.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Device Info')
|
||||
@Controller('device-info')
|
||||
export class DeviceInfoController {
|
||||
constructor(private readonly deviceInfoService: DeviceInfoService) {}
|
||||
|
||||
@Post()
|
||||
async create(@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
|
||||
return await this.deviceInfoService.create(createDeviceInfoDto, authUser);
|
||||
async createDeviceInfo(
|
||||
@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto,
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
): Promise<DeviceInfoResponseDto> {
|
||||
return this.deviceInfoService.create(createDeviceInfoDto, authUser);
|
||||
}
|
||||
|
||||
@Patch()
|
||||
async update(@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
|
||||
async updateDeviceInfo(
|
||||
@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto,
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
): Promise<DeviceInfoResponseDto> {
|
||||
return this.deviceInfoService.update(authUser.id, updateDeviceInfoDto);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
|
||||
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
|
||||
import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
|
||||
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto/create-device-info-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceInfoService {
|
||||
@@ -13,7 +14,7 @@ export class DeviceInfoService {
|
||||
private deviceRepository: Repository<DeviceInfoEntity>,
|
||||
) {}
|
||||
|
||||
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto) {
|
||||
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto): Promise<DeviceInfoResponseDto> {
|
||||
const res = await this.deviceRepository.findOne({
|
||||
where: {
|
||||
deviceId: createDeviceInfoDto.deviceId,
|
||||
@@ -23,7 +24,7 @@ export class DeviceInfoService {
|
||||
|
||||
if (res) {
|
||||
Logger.log('Device Info Exist', 'createDeviceInfo');
|
||||
return res;
|
||||
return mapDeviceInfoResponse(res);
|
||||
}
|
||||
|
||||
const deviceInfo = new DeviceInfoEntity();
|
||||
@@ -31,20 +32,18 @@ export class DeviceInfoService {
|
||||
deviceInfo.deviceType = createDeviceInfoDto.deviceType;
|
||||
deviceInfo.userId = authUser.id;
|
||||
|
||||
try {
|
||||
return await this.deviceRepository.save(deviceInfo);
|
||||
} catch (e) {
|
||||
Logger.error('Error creating new device info', 'createDeviceInfo');
|
||||
}
|
||||
const newDeviceInfo = await this.deviceRepository.save(deviceInfo);
|
||||
|
||||
return mapDeviceInfoResponse(newDeviceInfo);
|
||||
}
|
||||
|
||||
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto) {
|
||||
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto): Promise<DeviceInfoResponseDto> {
|
||||
const deviceInfo = await this.deviceRepository.findOne({
|
||||
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
|
||||
});
|
||||
|
||||
if (!deviceInfo) {
|
||||
throw new BadRequestException('Device Not Found');
|
||||
throw new NotFoundException('Device Not Found');
|
||||
}
|
||||
|
||||
const res = await this.deviceRepository.update(
|
||||
@@ -55,9 +54,15 @@ export class DeviceInfoService {
|
||||
);
|
||||
|
||||
if (res.affected == 1) {
|
||||
return await this.deviceRepository.findOne({
|
||||
const updatedDeviceInfo = await this.deviceRepository.findOne({
|
||||
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
|
||||
});
|
||||
|
||||
if (!updatedDeviceInfo) {
|
||||
throw new NotFoundException('Device Not Found');
|
||||
}
|
||||
|
||||
return mapDeviceInfoResponse(updatedDeviceInfo);
|
||||
} else {
|
||||
throw new BadRequestException('Bad Request');
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import { DeviceInfoEntity, DeviceType } from '@app/database/entities/device-info.entity';
|
||||
|
||||
export class DeviceInfoResponseDto {
|
||||
id!: number;
|
||||
userId!: string;
|
||||
deviceId!: string;
|
||||
deviceType!: DeviceType;
|
||||
notificationToken!: string | null;
|
||||
createdAt!: string;
|
||||
isAutoBackup!: boolean;
|
||||
}
|
||||
|
||||
export function mapDeviceInfoResponse(entity: DeviceInfoEntity): DeviceInfoResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
userId: entity.userId,
|
||||
deviceId: entity.deviceId,
|
||||
deviceType: entity.deviceType,
|
||||
notificationToken: entity.notificationToken,
|
||||
createdAt: entity.createdAt,
|
||||
isAutoBackup: entity.isAutoBackup,
|
||||
};
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
// TODO: this is being used as a response DTO. Should be changed to interface
|
||||
export class ServerInfoDto {
|
||||
export class ServerInfoResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
diskAvailable!: string;
|
@@ -0,0 +1,10 @@
|
||||
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerPingResponse {
|
||||
constructor(res: string) {
|
||||
this.res = res;
|
||||
}
|
||||
|
||||
@ApiResponseProperty({ type: String, example: 'pong' })
|
||||
res!: string;
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
import { IServerVersion } from 'apps/immich/src/constants/server_version.constant';
|
||||
|
||||
export class ServerVersionReponseDto implements IServerVersion {
|
||||
major!: number;
|
||||
minor!: number;
|
||||
patch!: number;
|
||||
build!: number;
|
||||
}
|
@@ -3,34 +3,28 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { serverVersion } from '../../constants/server_version.constant';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {}
|
||||
|
||||
@Get()
|
||||
async getServerInfo() {
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return await this.serverInfoService.getServerInfo();
|
||||
}
|
||||
|
||||
@Get('/ping')
|
||||
async getServerPulse() {
|
||||
return {
|
||||
res: 'pong',
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('/mapbox')
|
||||
async getMapboxInfo() {
|
||||
return {
|
||||
isEnable: this.configService.get('ENABLE_MAPBOX'),
|
||||
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
||||
};
|
||||
async pingServer(): Promise<ServerPingResponse> {
|
||||
return new ServerPingResponse('pong');
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
async getServerVersion() {
|
||||
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
||||
return serverVersion;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServerInfoDto } from './dto/server-info.dto';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import diskusage from 'diskusage';
|
||||
import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
|
||||
|
||||
@@ -10,7 +10,7 @@ export class ServerInfoService {
|
||||
|
||||
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
|
||||
|
||||
const serverInfo = new ServerInfoDto();
|
||||
const serverInfo = new ServerInfoResponseDto();
|
||||
serverInfo.diskAvailable = ServerInfoService.getHumanReadableString(diskInfo.available);
|
||||
serverInfo.diskSize = ServerInfoService.getHumanReadableString(diskInfo.total);
|
||||
serverInfo.diskUse = ServerInfoService.getHumanReadableString(diskInfo.total - diskInfo.free);
|
||||
|
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateProfileImageDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
file: any;
|
||||
}
|
@@ -1,27 +1,20 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'testuser@email.com' })
|
||||
email!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'password' })
|
||||
password!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'John' })
|
||||
firstName!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: 'Doe' })
|
||||
lastName!: string;
|
||||
|
||||
@IsOptional()
|
||||
profileImagePath?: string;
|
||||
|
||||
@IsOptional()
|
||||
isAdmin?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
shouldChangePassword?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
id?: string;
|
||||
}
|
||||
|
@@ -1,4 +1,24 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
export class UpdateUserDto {
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
@IsOptional()
|
||||
password?: string;
|
||||
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@IsOptional()
|
||||
isAdmin?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
shouldChangePassword?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
profileImagePath?: string;
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
export class CreateProfileImageResponseDto {
|
||||
userId!: string;
|
||||
profileImagePath!: string;
|
||||
}
|
||||
|
||||
export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
|
||||
return {
|
||||
userId: userId,
|
||||
profileImagePath: profileImagePath,
|
||||
};
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
|
||||
export class UserCountResponseDto {
|
||||
userCount!: number;
|
||||
}
|
||||
|
||||
export function mapUserCountResponse(count: number): UserCountResponseDto {
|
||||
return {
|
||||
userCount: count,
|
||||
};
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity';
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
createdAt: string;
|
||||
export class UserResponseDto {
|
||||
id!: string;
|
||||
email!: string;
|
||||
firstName!: string;
|
||||
lastName!: string;
|
||||
createdAt!: string;
|
||||
profileImagePath!: string;
|
||||
shouldChangePassword!: boolean;
|
||||
isAdmin!: boolean;
|
||||
}
|
||||
|
||||
export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
@@ -15,5 +18,8 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
firstName: entity.firstName,
|
||||
lastName: entity.lastName,
|
||||
createdAt: entity.createdAt,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
shouldChangePassword: entity.shouldChangePassword,
|
||||
isAdmin: entity.isAdmin,
|
||||
};
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
Response,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
@@ -21,50 +22,72 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { profileImageUploadOption } from '../../config/profile-image-upload.config';
|
||||
import { Response as Res } from 'express';
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { UserResponseDto } from './response-dto/user-response.dto';
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
import { UserCountResponseDto } from './response-dto/user-count-response.dto';
|
||||
import { CreateProfileImageDto } from './dto/create-profile-image.dto';
|
||||
import { CreateProfileImageResponseDto } from './response-dto/create-profile-image-response.dto';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get()
|
||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean) {
|
||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
|
||||
return await this.userService.getAllUsers(authUser, isAll);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('me')
|
||||
async getUserInfo(@GetAuthUser() authUser: AuthUserDto) {
|
||||
async getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
|
||||
return await this.userService.getUserInfo(authUser);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AdminRolesGuard)
|
||||
@Post()
|
||||
async createNewUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
|
||||
async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
||||
return await this.userService.createUser(createUserDto);
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
|
||||
async getUserCount(@Query('isAdmin') isAdmin: boolean): Promise<UserCountResponseDto> {
|
||||
return await this.userService.getUserCount(isAdmin);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Put()
|
||||
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
|
||||
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
||||
return await this.userService.updateUser(updateUserDto);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
type: CreateProfileImageDto,
|
||||
})
|
||||
@Post('/profile-image')
|
||||
async createProfileImage(@GetAuthUser() authUser: AuthUserDto, @UploadedFile() fileInfo: Express.Multer.File) {
|
||||
async createProfileImage(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@UploadedFile() fileInfo: Express.Multer.File,
|
||||
): Promise<CreateProfileImageResponseDto> {
|
||||
return await this.userService.createProfileImage(authUser, fileInfo);
|
||||
}
|
||||
|
||||
@Get('/profile-image/:userId')
|
||||
async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res) {
|
||||
return await this.userService.getUserProfileImage(userId, res);
|
||||
async getProfileImage(
|
||||
@Param('userId') userId: string,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
): Promise<StreamableFile | undefined> {
|
||||
return this.userService.getUserProfileImage(userId, res);
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,11 @@ import * as bcrypt from 'bcrypt';
|
||||
import { createReadStream } from 'fs';
|
||||
import { Response as Res } from 'express';
|
||||
import { mapUser, UserResponseDto } from './response-dto/user-response.dto';
|
||||
import { mapUserCountResponse, UserCountResponseDto } from './response-dto/user-count-response.dto';
|
||||
import {
|
||||
CreateProfileImageResponseDto,
|
||||
mapCreateProfileImageResponse,
|
||||
} from './response-dto/create-profile-image-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -24,24 +29,32 @@ export class UserService {
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
async getAllUsers(authUser: AuthUserDto, isAll: boolean) {
|
||||
async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
|
||||
if (isAll) {
|
||||
return await this.userRepository.find();
|
||||
const allUsers = await this.userRepository.find();
|
||||
|
||||
return allUsers.map(mapUser);
|
||||
}
|
||||
|
||||
return await this.userRepository.find({
|
||||
const allUserExceptRequestedUser = await this.userRepository.find({
|
||||
where: { id: Not(authUser.id) },
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
return allUserExceptRequestedUser.map(mapUser);
|
||||
}
|
||||
|
||||
async getUserInfo(authUser: AuthUserDto) {
|
||||
return this.userRepository.findOne({ where: { id: authUser.id } });
|
||||
async getUserInfo(authUser: AuthUserDto): Promise<UserResponseDto> {
|
||||
const user = await this.userRepository.findOne({ where: { id: authUser.id } });
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
return mapUser(user);
|
||||
}
|
||||
|
||||
async getUserCount(isAdmin: boolean) {
|
||||
async getUserCount(isAdmin: boolean): Promise<UserCountResponseDto> {
|
||||
let users;
|
||||
|
||||
if (isAdmin) {
|
||||
@@ -50,9 +63,7 @@ export class UserService {
|
||||
users = await this.userRepository.find();
|
||||
}
|
||||
|
||||
return {
|
||||
userCount: users.length,
|
||||
};
|
||||
return mapUserCountResponse(users.length);
|
||||
}
|
||||
|
||||
async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
||||
@@ -84,7 +95,7 @@ export class UserService {
|
||||
return bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
async updateUser(updateUserDto: UpdateUserDto) {
|
||||
async updateUser(updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
||||
const user = await this.userRepository.findOne({ where: { id: updateUserDto.id } });
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
@@ -115,31 +126,23 @@ export class UserService {
|
||||
try {
|
||||
const updatedUser = await this.userRepository.save(user);
|
||||
|
||||
// TODO: this should probably retrun UserResponseDto
|
||||
return {
|
||||
id: updatedUser.id,
|
||||
email: updatedUser.email,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
profileImagePath: updatedUser.profileImagePath,
|
||||
};
|
||||
return mapUser(updatedUser);
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Create new user');
|
||||
throw new InternalServerErrorException('Failed to register new user');
|
||||
}
|
||||
}
|
||||
|
||||
async createProfileImage(authUser: AuthUserDto, fileInfo: Express.Multer.File) {
|
||||
async createProfileImage(
|
||||
authUser: AuthUserDto,
|
||||
fileInfo: Express.Multer.File,
|
||||
): Promise<CreateProfileImageResponseDto> {
|
||||
try {
|
||||
await this.userRepository.update(authUser.id, {
|
||||
profileImagePath: fileInfo.path,
|
||||
});
|
||||
|
||||
return {
|
||||
userId: authUser.id,
|
||||
profileImagePath: fileInfo.path,
|
||||
};
|
||||
return mapCreateProfileImageResponse(authUser.id, fileInfo.path);
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Create User Profile Image');
|
||||
throw new InternalServerErrorException('Failed to create new user profile image');
|
||||
|
@@ -1,7 +1,14 @@
|
||||
// major.minor.patch+build
|
||||
// check mobile/pubspec.yml for current release version
|
||||
|
||||
export const serverVersion = {
|
||||
export interface IServerVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
build: number;
|
||||
}
|
||||
|
||||
export const serverVersion: IServerVersion = {
|
||||
major: 1,
|
||||
minor: 17,
|
||||
patch: 0,
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||
|
||||
@@ -15,6 +18,38 @@ async function bootstrap() {
|
||||
|
||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Immich')
|
||||
.setDescription('Immich API')
|
||||
.setVersion('1.17.0')
|
||||
.addBearerAuth({
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
name: 'JWT',
|
||||
description: 'Enter JWT token',
|
||||
in: 'header',
|
||||
})
|
||||
.addServer('/api')
|
||||
.build();
|
||||
|
||||
const apiDocumentOptions: SwaggerDocumentOptions = {
|
||||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||
};
|
||||
|
||||
const apiDocument = SwaggerModule.createDocument(app, config, apiDocumentOptions);
|
||||
|
||||
SwaggerModule.setup('doc', app, apiDocument, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
customSiteTitle: 'Immich API Documentation',
|
||||
});
|
||||
|
||||
// Generate API Documentation
|
||||
const outputPath = path.resolve(process.cwd(), 'immich-openapi-specs.json');
|
||||
writeFileSync(outputPath, JSON.stringify(apiDocument), { encoding: 'utf8' });
|
||||
|
||||
await app.listen(3001, () => {
|
||||
if (process.env.NODE_ENV == 'development') {
|
||||
Logger.log('Running Immich Server in DEVELOPMENT environment', 'ImmichServer');
|
||||
|
@@ -5,6 +5,7 @@ import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import fs from 'fs';
|
||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||
import { Job } from 'bull';
|
||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
||||
|
||||
@Processor('background-task')
|
||||
export class BackgroundTaskProcessor {
|
||||
@@ -18,7 +19,7 @@ export class BackgroundTaskProcessor {
|
||||
|
||||
// TODO: Should probably use constants / Interfaces for Queue names / data
|
||||
@Process('delete-file-on-disk')
|
||||
async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) {
|
||||
async deleteFileOnDisk(job: Job<{ assets: AssetResponseDto[] }>) {
|
||||
const { assets } = job.data;
|
||||
|
||||
for (const asset of assets) {
|
||||
|
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Queue } from 'bull';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BackgroundTaskService {
|
||||
@@ -11,7 +12,7 @@ export class BackgroundTaskService {
|
||||
private backgroundTaskQueue: Queue,
|
||||
) {}
|
||||
|
||||
async deleteFileOnDisk(assets: AssetEntity[]) {
|
||||
async deleteFileOnDisk(assets: AssetResponseDto[]) {
|
||||
await this.backgroundTaskQueue.add(
|
||||
'delete-file-on-disk',
|
||||
{
|
||||
|
@@ -63,7 +63,7 @@ export class AssetUploadedProcessor {
|
||||
}
|
||||
|
||||
// Extract video duration if uploaded from the web & CLI
|
||||
if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
|
||||
if (asset.type == AssetType.VIDEO) {
|
||||
await this.metadataExtractionQueue.add(videoMetadataExtractionProcessorName, { asset }, { jobId: randomUUID() });
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user