mirror of
https://github.com/immich-app/immich.git
synced 2024-11-16 02:18:50 -07:00
refactor(server): user profile picture (#4728)
This commit is contained in:
parent
431536cdbb
commit
3212a47720
@ -123,15 +123,6 @@ export class UserCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProfileImage(authUser: AuthUserDto, filePath: string): Promise<UserEntity> {
|
|
||||||
try {
|
|
||||||
return this.userRepository.update(authUser.id, { profileImagePath: filePath });
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error(e, 'Create User Profile Image');
|
|
||||||
throw new InternalServerErrorException('Failed to create new user profile image');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreUser(authUser: AuthUserDto, userToRestore: UserEntity): Promise<UserEntity> {
|
async restoreUser(authUser: AuthUserDto, userToRestore: UserEntity): Promise<UserEntity> {
|
||||||
if (!authUser.isAdmin) {
|
if (!authUser.isAdmin) {
|
||||||
throw new ForbiddenException('Unauthorized');
|
throw new ForbiddenException('Unauthorized');
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
userStub,
|
userStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
|
import { Readable } from 'stream';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { JobName } from '../job';
|
import { JobName } from '../job';
|
||||||
import {
|
import {
|
||||||
@ -461,7 +462,7 @@ describe(UserService.name, () => {
|
|||||||
it('should throw an error if the user does not exist', async () => {
|
it('should throw an error if the user does not exist', async () => {
|
||||||
userMock.get.mockResolvedValue(null);
|
userMock.get.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(sut.getProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);
|
await expect(sut.getProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id);
|
expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id);
|
||||||
});
|
});
|
||||||
@ -473,6 +474,18 @@ describe(UserService.name, () => {
|
|||||||
|
|
||||||
expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id);
|
expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return the profile picture', async () => {
|
||||||
|
const stream = new Readable();
|
||||||
|
|
||||||
|
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||||
|
storageMock.createReadStream.mockResolvedValue({ stream });
|
||||||
|
|
||||||
|
await expect(sut.getProfileImage(userStub.profilePath.id)).resolves.toEqual({ stream });
|
||||||
|
|
||||||
|
expect(userMock.get).toHaveBeenCalledWith(userStub.profilePath.id);
|
||||||
|
expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/profile.jpg', 'image/jpeg');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetAdminPassword', () => {
|
describe('resetAdminPassword', () => {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { UserEntity } from '@app/infra/entities';
|
import { UserEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { ReadStream, constants, createReadStream } from 'fs';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { IEntityJob, JobName } from '../job';
|
import { IEntityJob, JobName } from '../job';
|
||||||
import {
|
import {
|
||||||
@ -13,6 +11,7 @@ import {
|
|||||||
ILibraryRepository,
|
ILibraryRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
ImmichReadStream,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from '../storage';
|
import { StorageCore, StorageFolder } from '../storage';
|
||||||
import { CreateUserDto, UpdateUserDto } from './dto';
|
import { CreateUserDto, UpdateUserDto } from './dto';
|
||||||
@ -41,8 +40,8 @@ export class UserService {
|
|||||||
return users.map(mapUser);
|
return users.map(mapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(userId: string, withDeleted = false): Promise<UserResponseDto> {
|
async get(userId: string): Promise<UserResponseDto> {
|
||||||
const user = await this.userRepository.get(userId, withDeleted);
|
const user = await this.userRepository.get(userId, false);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
@ -97,20 +96,16 @@ export class UserService {
|
|||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
fileInfo: Express.Multer.File,
|
fileInfo: Express.Multer.File,
|
||||||
): Promise<CreateProfileImageResponseDto> {
|
): Promise<CreateProfileImageResponseDto> {
|
||||||
const updatedUser = await this.userCore.createProfileImage(authUser, fileInfo.path);
|
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
|
||||||
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
|
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProfileImage(userId: string): Promise<ReadStream> {
|
async getProfileImage(id: string): Promise<ImmichReadStream> {
|
||||||
const user = await this.userRepository.get(userId);
|
const user = await this.findOrFail(id);
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
if (!user.profileImagePath) {
|
if (!user.profileImagePath) {
|
||||||
throw new NotFoundException('User does not have a profile image');
|
throw new NotFoundException('User does not have a profile image');
|
||||||
}
|
}
|
||||||
await fs.access(user.profileImagePath, constants.R_OK);
|
return this.storageRepository.createReadStream(user.profileImagePath, 'image/jpeg');
|
||||||
return createReadStream(user.profileImagePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
|
async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
|
||||||
@ -185,4 +180,12 @@ export class UserService {
|
|||||||
|
|
||||||
return msSinceDelete >= msDeleteWait;
|
return msSinceDelete >= msDeleteWait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async findOrFail(id: string) {
|
||||||
|
const user = await this.userRepository.get(id);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('User not found');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,13 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
Response,
|
|
||||||
StreamableFile,
|
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response as Res } from 'express';
|
|
||||||
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
|
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
|
||||||
import { FileUploadInterceptor, Route } from '../app.interceptor';
|
import { FileUploadInterceptor, Route } from '../app.interceptor';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation, asStreamableFile } from '../app.utils';
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('User')
|
@ApiTags('User')
|
||||||
@ -88,9 +85,7 @@ export class UserController {
|
|||||||
|
|
||||||
@Get('profile-image/:id')
|
@Get('profile-image/:id')
|
||||||
@Header('Cache-Control', 'private, no-cache, no-transform')
|
@Header('Cache-Control', 'private, no-cache, no-transform')
|
||||||
async getProfileImage(@Param() { id }: UUIDParamDto, @Response({ passthrough: true }) res: Res): Promise<any> {
|
getProfileImage(@Param() { id }: UUIDParamDto): Promise<any> {
|
||||||
const readableStream = await this.service.getProfileImage(id);
|
return this.service.getProfileImage(id).then(asStreamableFile);
|
||||||
res.header('Content-Type', 'image/jpeg');
|
|
||||||
return new StreamableFile(readableStream);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
server/test/fixtures/user.stub.ts
vendored
17
server/test/fixtures/user.stub.ts
vendored
@ -104,4 +104,21 @@ export const userStub = {
|
|||||||
assets: [],
|
assets: [],
|
||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
}),
|
}),
|
||||||
|
profilePath: Object.freeze<UserEntity>({
|
||||||
|
...authStub.user1,
|
||||||
|
password: 'immich_password',
|
||||||
|
firstName: 'immich_first_name',
|
||||||
|
lastName: 'immich_last_name',
|
||||||
|
storageLabel: 'label-1',
|
||||||
|
externalPath: null,
|
||||||
|
oauthId: '',
|
||||||
|
shouldChangePassword: false,
|
||||||
|
profileImagePath: '/path/to/profile.jpg',
|
||||||
|
createdAt: new Date('2021-01-01'),
|
||||||
|
deletedAt: null,
|
||||||
|
updatedAt: new Date('2021-01-01'),
|
||||||
|
tags: [],
|
||||||
|
assets: [],
|
||||||
|
memoriesEnabled: true,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user