1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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:
Jason Rasmussen 2023-11-03 21:33:15 -04:00 committed by GitHub
parent d4ef6f52bb
commit 2e424fe249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1337 additions and 1315 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ class OAuthService {
// Resolve API server endpoint from user provided serverUrl
await _apiService.resolveAndSetEndpoint(serverUrl);
return await _apiService.oAuthApi.generateConfig(
return await _apiService.oAuthApi.generateOAuthConfig(
OAuthConfigDto(redirectUri: '$callbackUrlScheme:/'),
);
}
@ -29,7 +29,7 @@ class OAuthService {
callbackUrlScheme: callbackUrlScheme,
);
return await _apiService.oAuthApi.callback(
return await _apiService.oAuthApi.finishOAuth(
OAuthCallbackDto(
url: result,
),

View File

@ -65,7 +65,7 @@ class LoginForm extends HookConsumerWidget {
isLoadingServer.value = true;
final endpoint = await apiService.resolveAndSetEndpoint(serverUrl);
final loginConfig = await apiService.oAuthApi.generateConfig(
final loginConfig = await apiService.oAuthApi.generateOAuthConfig(
OAuthConfigDto(redirectUri: serverUrl),
);

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -678,7 +678,7 @@
},
"/api-key": {
"get": {
"operationId": "getKeys",
"operationId": "getApiKeys",
"parameters": [],
"responses": {
"200": {
@ -711,7 +711,7 @@
]
},
"post": {
"operationId": "createKey",
"operationId": "createApiKey",
"parameters": [],
"requestBody": {
"content": {
@ -753,7 +753,7 @@
},
"/api-key/{id}": {
"delete": {
"operationId": "deleteKey",
"operationId": "deleteApiKey",
"parameters": [
{
"name": "id",
@ -786,7 +786,7 @@
]
},
"get": {
"operationId": "getKey",
"operationId": "getApiKey",
"parameters": [
{
"name": "id",
@ -826,7 +826,7 @@
]
},
"put": {
"operationId": "updateKey",
"operationId": "updateApiKey",
"parameters": [
{
"name": "id",
@ -1084,7 +1084,7 @@
"/asset/bulk-upload-check": {
"post": {
"description": "Checks if assets exist by checksums",
"operationId": "bulkUploadCheck",
"operationId": "checkBulkUpload",
"parameters": [],
"requestBody": {
"content": {
@ -1855,7 +1855,7 @@
},
"/asset/statistics": {
"get": {
"operationId": "getAssetStats",
"operationId": "getAssetStatistics",
"parameters": [
{
"name": "isArchived",
@ -1977,7 +1977,7 @@
},
"/asset/time-bucket": {
"get": {
"operationId": "getByTimeBucket",
"operationId": "getTimeBucket",
"parameters": [
{
"name": "size",
@ -2596,7 +2596,7 @@
},
"/auth/admin-sign-up": {
"post": {
"operationId": "adminSignUp",
"operationId": "signUpAdmin",
"parameters": [],
"requestBody": {
"content": {
@ -2943,7 +2943,7 @@
},
"/library": {
"get": {
"operationId": "getAllForUser",
"operationId": "getLibraries",
"parameters": [],
"responses": {
"200": {
@ -3265,7 +3265,7 @@
},
"/oauth/authorize": {
"post": {
"operationId": "authorizeOAuth",
"operationId": "startOAuth",
"parameters": [],
"requestBody": {
"content": {
@ -3296,7 +3296,7 @@
},
"/oauth/callback": {
"post": {
"operationId": "callback",
"operationId": "finishOAuth",
"parameters": [],
"requestBody": {
"content": {
@ -3329,7 +3329,7 @@
"post": {
"deprecated": true,
"description": "@deprecated use feature flags and /oauth/authorize",
"operationId": "generateConfig",
"operationId": "generateOAuthConfig",
"parameters": [],
"requestBody": {
"content": {
@ -3360,7 +3360,7 @@
},
"/oauth/link": {
"post": {
"operationId": "link",
"operationId": "linkOAuthAccount",
"parameters": [],
"requestBody": {
"content": {
@ -3402,7 +3402,7 @@
},
"/oauth/mobile-redirect": {
"get": {
"operationId": "mobileRedirect",
"operationId": "redirectOAuthToMobile",
"parameters": [],
"responses": {
"200": {
@ -3416,7 +3416,7 @@
},
"/oauth/unlink": {
"post": {
"operationId": "unlink",
"operationId": "unlinkOAuthAccount",
"parameters": [],
"responses": {
"201": {
@ -4307,9 +4307,9 @@
]
}
},
"/server-info/stats": {
"/server-info/statistics": {
"get": {
"operationId": "getStats",
"operationId": "getServerStatistics",
"parameters": [],
"responses": {
"200": {
@ -4837,7 +4837,7 @@
},
"/system-config/defaults": {
"get": {
"operationId": "getDefaults",
"operationId": "getConfigDefaults",
"parameters": [],
"responses": {
"200": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ import {
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../app.interceptor';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { AssetService } from './asset.service';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
@ -204,7 +204,7 @@ export class AssetController {
*/
@Post('/bulk-upload-check')
@HttpCode(HttpStatus.OK)
bulkUploadCheck(
checkBulkUpload(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {

View File

@ -2,14 +2,13 @@ import { DomainModule } from '@app/domain';
import { InfraModule } from '@app/infra';
import { AssetEntity } from '@app/infra/entities';
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository';
import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
import { AssetService } from './api-v1/asset/asset.service';
import { AppGuard } from './app.guard';
import { FileUploadInterceptor } from './app.interceptor';
import { AppService } from './app.service';
import {
APIKeyController,
@ -31,6 +30,7 @@ import {
TagController,
UserController,
} from './controllers';
import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
@Module({
imports: [
@ -61,10 +61,9 @@ import {
PersonController,
],
providers: [
//
{ provide: APP_GUARD, useExisting: AppGuard },
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
{ provide: APP_GUARD, useClass: AppGuard },
{ provide: IAssetRepository, useClass: AssetRepository },
AppGuard,
AppService,
AssetService,
FileUploadInterceptor,

View File

@ -47,6 +47,9 @@ function sortKeys<T>(obj: T): T {
return result as T;
}
export const routeToErrorMessage = (methodName: string) =>
'Failed to ' + methodName.replace(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
const patchOpenAPI = (document: OpenAPIObject) => {
document.paths = sortKeys(document.paths);
if (document.components?.schemas) {
@ -78,6 +81,10 @@ const patchOpenAPI = (document: OpenAPIObject) => {
delete operation.summary;
}
if (operation.operationId) {
// console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
}
if (operation.description === '') {
delete operation.description;
}

View File

@ -20,22 +20,22 @@ export class APIKeyController {
constructor(private service: APIKeyService) {}
@Post()
createKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
createApiKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto);
}
@Get()
getKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
getApiKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser);
}
@Get(':id')
getKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
getApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
}
@Put(':id')
updateKey(
updateApiKey(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: APIKeyUpdateDto,
@ -44,7 +44,7 @@ export class APIKeyController {
}
@Delete(':id')
deleteKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
deleteApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
}
}

View File

@ -39,10 +39,11 @@ import {
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation, asStreamableFile } from '../app.utils';
import { Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('Asset')
@Controller('asset')
@Controller(Route.ASSET)
@Authenticated()
@UseValidation()
export class AssetController {
@ -86,7 +87,7 @@ export class AssetController {
}
@Get('statistics')
getAssetStats(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(authUser, dto);
}
@ -98,8 +99,8 @@ export class AssetController {
@Authenticated({ isShared: true })
@Get('time-bucket')
getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getByTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
getTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
}
@Post('jobs')

View File

@ -43,7 +43,7 @@ export class AuthController {
@PublicRoute()
@Post('admin-sign-up')
@ApiBadRequestResponse({ description: 'The server already has an admin' })
adminSignUp(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
signUpAdmin(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
return this.service.adminSignUp(signUpCredential);
}

View File

@ -21,7 +21,7 @@ export class LibraryController {
constructor(private service: LibraryService) {}
@Get()
getAllForUser(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> {
getLibraries(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> {
return this.service.getAllForUser(authUser);
}

View File

@ -25,7 +25,7 @@ export class OAuthController {
@PublicRoute()
@Get('mobile-redirect')
@Redirect()
mobileRedirect(@Req() req: Request) {
redirectOAuthToMobile(@Req() req: Request) {
return {
url: this.service.getMobileRedirect(req.url),
statusCode: HttpStatus.TEMPORARY_REDIRECT,
@ -35,19 +35,19 @@ export class OAuthController {
/** @deprecated use feature flags and /oauth/authorize */
@PublicRoute()
@Post('config')
generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
generateOAuthConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
return this.service.generateConfig(dto);
}
@PublicRoute()
@Post('authorize')
authorizeOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute()
@Post('callback')
async callback(
async finishOAuth(
@Res({ passthrough: true }) res: Response,
@Body() dto: OAuthCallbackDto,
@GetLoginDetails() loginDetails: LoginDetails,
@ -58,12 +58,12 @@ export class OAuthController {
}
@Post('link')
link(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
linkOAuthAccount(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(authUser, dto);
}
@Post('unlink')
unlink(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
unlinkOAuthAccount(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return this.service.unlink(authUser);
}
}

View File

@ -57,9 +57,9 @@ export class ServerInfoController {
}
@AdminRoute()
@Get('stats')
getStats(): Promise<ServerStatsResponseDto> {
return this.service.getStats();
@Get('statistics')
getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics();
}
@PublicRoute()

View File

@ -17,7 +17,7 @@ export class SystemConfigController {
}
@Get('defaults')
getDefaults(): SystemConfigDto {
getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults();
}

View File

@ -22,8 +22,8 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
import { FileUploadInterceptor, Route } from '../app.interceptor';
import { UseValidation, asStreamableFile } from '../app.utils';
import { FileUploadInterceptor, Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('User')

View File

@ -0,0 +1,32 @@
import {
CallHandler,
ExecutionContext,
HttpException,
Injectable,
InternalServerErrorException,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, catchError, throwError } from 'rxjs';
import { routeToErrorMessage } from '../app.utils';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
private logger = new Logger(ErrorInterceptor.name);
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
return next.handle().pipe(
catchError((error) =>
throwError(() => {
if (error instanceof HttpException === false) {
const errorMessage = routeToErrorMessage(context.getHandler().name);
this.logger.error(errorMessage, error, error?.errors);
return new InternalServerErrorException(errorMessage);
} else {
return error;
}
}),
),
);
}
}

View File

@ -7,7 +7,7 @@ import { createHash } from 'crypto';
import { NextFunction, RequestHandler } from 'express';
import multer, { StorageEngine, diskStorage } from 'multer';
import { Observable } from 'rxjs';
import { AuthRequest } from './app.guard';
import { AuthRequest } from '../app.guard';
export enum Route {
ASSET = 'asset',

View File

@ -0,0 +1,2 @@
export * from './error.interceptor';
export * from './file.interceptor';

View File

@ -493,7 +493,7 @@ export class AssetRepository implements IAssetRepository {
.getRawMany();
}
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
const truncated = dateTrunc(options);
return (
this.getBuilder(options)

View File

@ -103,9 +103,9 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
});
});
describe('GET /server-info/stats', () => {
describe('GET /server-info/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/server-info/stats');
const { status, body } = await request(server).get('/server-info/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
@ -115,7 +115,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
await api.userApi.create(server, accessToken, { ...loginDto, firstName: 'test', lastName: 'test' });
const { accessToken: userAccessToken } = await api.authApi.login(server, loginDto);
const { status, body } = await request(server)
.get('/server-info/stats')
.get('/server-info/statistics')
.set('Authorization', `Bearer ${userAccessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorStub.forbidden);
@ -123,7 +123,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
it('should return the server stats', async () => {
const { status, body } = await request(server)
.get('/server-info/stats')
.get('/server-info/statistics')
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({

View File

@ -26,7 +26,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
findLivePhotoMatch: jest.fn(),
getMapMarkers: jest.fn(),
getStatistics: jest.fn(),
getByTimeBucket: jest.fn(),
getTimeBucket: jest.fn(),
getTimeBuckets: jest.fn(),
restoreAll: jest.fn(),
softDeleteAll: jest.fn(),

File diff suppressed because it is too large Load Diff

View File

@ -36,23 +36,19 @@ export const oauth = {
authorize: async (location: Location) => {
try {
const redirectUri = location.href.split('?')[0];
const { data } = await api.oauthApi.authorizeOAuth({ oAuthConfigDto: { redirectUri } });
const { data } = await api.oauthApi.startOAuth({ oAuthConfigDto: { redirectUri } });
goto(data.url);
} catch (error) {
handleError(error, 'Unable to login with OAuth');
}
},
getConfig: (location: Location) => {
const redirectUri = location.href.split('?')[0];
return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } });
},
login: (location: Location) => {
return api.oauthApi.callback({ oAuthCallbackDto: { url: location.href } });
return api.oauthApi.finishOAuth({ oAuthCallbackDto: { url: location.href } });
},
link: (location: Location): AxiosPromise<UserResponseDto> => {
return api.oauthApi.link({ oAuthCallbackDto: { url: location.href } });
return api.oauthApi.linkOAuthAccount({ oAuthCallbackDto: { url: location.href } });
},
unlink: () => {
return api.oauthApi.unlink();
return api.oauthApi.unlinkOAuthAccount();
},
};

View File

@ -32,7 +32,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.ffmpeg),
]);
}
@ -76,7 +76,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
ffmpegConfig = { ...configs.ffmpeg };
defaultConfig = { ...configs.ffmpeg };

View File

@ -22,7 +22,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.job),
api.systemConfigApi.getDefaults().then((res) => res.data.job),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.job),
]);
}
@ -59,7 +59,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
jobConfig = { ...configs.job };
defaultConfig = { ...configs.job };

View File

@ -28,7 +28,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.library),
api.systemConfigApi.getDefaults().then((res) => res.data.library),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.library),
]);
}
@ -68,7 +68,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
libraryConfig = { ...configs.library };
defaultConfig = { ...configs.library };

View File

@ -22,7 +22,7 @@
async function refreshConfig() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.machineLearning),
api.systemConfigApi.getDefaults().then((res) => res.data.machineLearning),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.machineLearning),
]);
}

View File

@ -22,7 +22,7 @@
async function refreshConfig() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data),
api.systemConfigApi.getDefaults().then((res) => res.data),
api.systemConfigApi.getConfigDefaults().then((res) => res.data),
]);
}
@ -65,7 +65,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
config = cloneDeep(configs);
defaultConfig = cloneDeep(configs);

View File

@ -18,7 +18,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.newVersionCheck),
api.systemConfigApi.getDefaults().then((res) => res.data.newVersionCheck),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.newVersionCheck),
]);
}
@ -55,7 +55,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
newVersionCheckConfig = { ...configs.newVersionCheck };
defaultConfig = { ...configs.newVersionCheck };

View File

@ -29,7 +29,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
api.systemConfigApi.getDefaults().then((res) => res.data.oauth),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.oauth),
]);
}
@ -90,7 +90,7 @@
}
async function resetToDefault() {
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
const { data: defaultConfig } = await api.systemConfigApi.getConfigDefaults();
oauthConfig = { ...defaultConfig.oauth };

View File

@ -20,7 +20,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.passwordLogin),
]);
}
@ -77,7 +77,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
passwordLoginConfig = { ...configs.passwordLogin };
defaultConfig = { ...configs.passwordLogin };

View File

@ -26,7 +26,7 @@
async function getConfigs() {
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.storageTemplate),
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data),
]);
@ -119,7 +119,7 @@
}
async function resetToDefault() {
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
const { data: defaultConfig } = await api.systemConfigApi.getConfigDefaults();
storageConfig.template = defaultConfig.storageTemplate.template;

View File

@ -19,7 +19,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.theme),
api.systemConfigApi.getDefaults().then((res) => res.data.theme),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.theme),
]);
}
@ -56,7 +56,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
themeConfig = { ...configs.theme };
defaultConfig = { ...configs.theme };

View File

@ -20,7 +20,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.thumbnail),
api.systemConfigApi.getDefaults().then((res) => res.data.thumbnail),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.thumbnail),
]);
}
@ -37,7 +37,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
thumbnailConfig = { ...configs.thumbnail };
defaultConfig = { ...configs.thumbnail };

View File

@ -20,7 +20,7 @@
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.trash),
api.systemConfigApi.getDefaults().then((res) => res.data.trash),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.trash),
]);
}
@ -53,7 +53,7 @@
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
trashConfig = { ...configs.trash };
defaultConfig = { ...configs.trash };

View File

@ -30,7 +30,7 @@
const firstName = form.get('firstName');
const lastName = form.get('lastName');
const { status } = await api.authenticationApi.adminSignUp({
const { status } = await api.authenticationApi.signUpAdmin({
signUpDto: {
email: String(email),
password: String(password),

View File

@ -2,7 +2,7 @@
import { page } from '$app/stores';
import { locale, sidebarSettings } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { AssetApiGetAssetStatsRequest, api } from '@api';
import { AssetApiGetAssetStatisticsRequest, api } from '@api';
import {
mdiAccount,
mdiAccountMultiple,
@ -23,8 +23,8 @@
import SideBarButton from './side-bar-button.svelte';
import SideBarSection from './side-bar-section.svelte';
const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
const { data: stats } = await api.assetApi.getAssetStats(dto);
const getStats = async (dto: AssetApiGetAssetStatisticsRequest) => {
const { data: stats } = await api.assetApi.getAssetStatistics(dto);
return stats;
};

View File

@ -82,7 +82,7 @@
};
async function readLibraryList() {
const { data } = await api.libraryApi.getAllForUser();
const { data } = await api.libraryApi.getLibraries();
libraries = data;
dropdownOpen.length = libraries.length;

View File

@ -25,14 +25,14 @@
};
async function refreshKeys() {
const { data } = await api.keyApi.getKeys();
const { data } = await api.keyApi.getApiKeys();
keys = data;
}
const handleCreate = async (event: CustomEvent<APIKeyResponseDto>) => {
try {
const dto = event.detail;
const { data } = await api.keyApi.createKey({ aPIKeyCreateDto: dto });
const { data } = await api.keyApi.createApiKey({ aPIKeyCreateDto: dto });
secret = data.secret;
} catch (error) {
handleError(error, 'Unable to create a new API Key');
@ -50,7 +50,7 @@
const dto = event.detail;
try {
await api.keyApi.updateKey({ id: editKey.id, aPIKeyUpdateDto: { name: dto.name } });
await api.keyApi.updateApiKey({ id: editKey.id, aPIKeyUpdateDto: { name: dto.name } });
notificationController.show({
message: `Saved API Key`,
type: NotificationType.Info,
@ -69,7 +69,7 @@
}
try {
await api.keyApi.deleteKey({ id: deleteKey.id });
await api.keyApi.deleteApiKey({ id: deleteKey.id });
notificationController.show({
message: `Removed API Key: ${deleteKey.name}`,
type: NotificationType.Info,

View File

@ -196,7 +196,7 @@ export class AssetStore {
bucket.cancelToken = new AbortController();
const { data: assets } = await api.assetApi.getByTimeBucket(
const { data: assets } = await api.assetApi.getTimeBucket(
{
...this.options,
timeBucket: bucketDate,
@ -206,7 +206,7 @@ export class AssetStore {
);
if (this.albumId) {
const { data: albumAssets } = await api.assetApi.getByTimeBucket(
const { data: albumAssets } = await api.assetApi.getTimeBucket(
{
albumId: this.albumId,
timeBucket: bucketDate,

View File

@ -8,7 +8,7 @@ export const load = (async ({ parent, locals }) => {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: keys } = await locals.api.keyApi.getKeys();
const { data: keys } = await locals.api.keyApi.getApiKeys();
const { data: devices } = await locals.api.authenticationApi.getAuthDevices();
const { data: partners } = await locals.api.partnerApi.getPartners({ direction: 'shared-by' });

View File

@ -11,7 +11,7 @@ export const load = (async ({ parent, locals: { api } }) => {
throw redirect(302, AppRoute.PHOTOS);
}
const { data: stats } = await api.serverInfoApi.getStats();
const { data: stats } = await api.serverInfoApi.getServerStatistics();
return {
user,

View File

@ -11,7 +11,7 @@
onMount(async () => {
setIntervalHandler = setInterval(async () => {
const { data: stats } = await api.serverInfoApi.getStats();
const { data: stats } = await api.serverInfoApi.getServerStatistics();
data.stats = stats;
}, 5000);
});