feat(server): better api error messages (for unhandled exceptions) (#4817)
* feat(server): better error messages * chore: open api * chore: remove debug log * fix: syntax error * fix: e2e test
This commit is contained in:
@@ -331,17 +331,17 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByTimeBucket', () => {
|
||||
describe('getTimeBucket', () => {
|
||||
it('should return the assets for a album time bucket if user has album.read', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getByTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
||||
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
|
||||
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-id');
|
||||
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
|
||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
albumId: 'album-id',
|
||||
@@ -349,17 +349,17 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getByTimeBucket(authStub.admin, {
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
userId: authStub.admin.id,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
|
||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
@@ -368,16 +368,16 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getByTimeBucket(authStub.admin, {
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
userId: authStub.admin.id,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
|
||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
userId: authStub.admin.id,
|
||||
|
||||
@@ -194,12 +194,12 @@ export class AssetService {
|
||||
return this.assetRepository.getTimeBuckets(dto);
|
||||
}
|
||||
|
||||
async getByTimeBucket(
|
||||
async getTimeBucket(
|
||||
authUser: AuthUserDto,
|
||||
dto: TimeBucketAssetDto,
|
||||
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
||||
await this.timeBucketChecks(authUser, dto);
|
||||
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
|
||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, dto);
|
||||
if (authUser.isShowMetadata) {
|
||||
return assets.map((asset) => mapAsset(asset, { withStack: true }));
|
||||
} else {
|
||||
|
||||
@@ -315,13 +315,8 @@ export class AuthService {
|
||||
const redirectUri = this.normalize(config, url.split('?')[0]);
|
||||
const client = await this.getOAuthClient(config);
|
||||
const params = client.callbackParams(url);
|
||||
try {
|
||||
const tokens = await client.callback(redirectUri, params, { state: params.state });
|
||||
return client.userinfo<OAuthProfile>(tokens.access_token || '');
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Unable to complete OAuth login: ${error}`, error?.stack);
|
||||
throw new InternalServerErrorException(`Unable to complete OAuth login: ${error}`, { cause: error });
|
||||
}
|
||||
const tokens = await client.callback(redirectUri, params, { state: params.state });
|
||||
return client.userinfo<OAuthProfile>(tokens.access_token || '');
|
||||
}
|
||||
|
||||
private async getOAuthClient(config: SystemConfig) {
|
||||
|
||||
@@ -123,6 +123,6 @@ export interface IAssetRepository {
|
||||
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ describe(ServerInfoService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.getStats()).resolves.toEqual({
|
||||
await expect(sut.getStatistics()).resolves.toEqual({
|
||||
photos: 120,
|
||||
videos: 31,
|
||||
usage: 1123455,
|
||||
|
||||
@@ -92,7 +92,7 @@ export class ServerInfoService {
|
||||
};
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
async getStatistics(): Promise<ServerStatsResponseDto> {
|
||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LibraryType, UserEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import path from 'path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AuthUserDto } from '../auth';
|
||||
@@ -61,26 +61,21 @@ export class UserCore {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (dto.password) {
|
||||
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
if (dto.storageLabel === '') {
|
||||
dto.storageLabel = null;
|
||||
}
|
||||
|
||||
if (dto.externalPath === '') {
|
||||
dto.externalPath = null;
|
||||
} else if (dto.externalPath) {
|
||||
dto.externalPath = path.normalize(dto.externalPath);
|
||||
}
|
||||
|
||||
return this.userRepository.update(id, dto);
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Failed to update user info');
|
||||
throw new InternalServerErrorException('Failed to update user info');
|
||||
if (dto.password) {
|
||||
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
if (dto.storageLabel === '') {
|
||||
dto.storageLabel = null;
|
||||
}
|
||||
|
||||
if (dto.externalPath === '') {
|
||||
dto.externalPath = null;
|
||||
} else if (dto.externalPath) {
|
||||
dto.externalPath = path.normalize(dto.externalPath);
|
||||
}
|
||||
|
||||
return this.userRepository.update(id, dto);
|
||||
}
|
||||
|
||||
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
|
||||
@@ -96,30 +91,25 @@ export class UserCore {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: Partial<UserEntity> = { ...dto };
|
||||
if (payload.password) {
|
||||
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||
}
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel);
|
||||
}
|
||||
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
owner: { id: userEntity.id } as UserEntity,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
isVisible: true,
|
||||
});
|
||||
|
||||
return userEntity;
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Create new user');
|
||||
throw new InternalServerErrorException('Failed to register new user');
|
||||
const payload: Partial<UserEntity> = { ...dto };
|
||||
if (payload.password) {
|
||||
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||
}
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel);
|
||||
}
|
||||
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
owner: { id: userEntity.id } as UserEntity,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
isVisible: true,
|
||||
});
|
||||
|
||||
return userEntity;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user