chore(server): remove deprecated endpoints (#6984)

* chore: remove deprecated endpoints

* chore: open api
This commit is contained in:
Jason Rasmussen
2024-02-08 16:57:54 -05:00
committed by GitHub
parent 198e8517e5
commit 90a7f16817
36 changed files with 168 additions and 2905 deletions
+53 -137
View File
@@ -14,7 +14,7 @@ import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/i
import { AssetRepository } from '@app/infra/repositories';
import { INestApplication } from '@nestjs/common';
import { errorStub, userDto, uuidStub } from '@test/fixtures';
import { randomBytes } from 'crypto';
import { randomBytes } from 'node:crypto';
import request from 'supertest';
import { api } from '../../client';
import { generateAsset, testApp, today, yesterday } from '../utils';
@@ -517,91 +517,6 @@ describe(`${AssetController.name} (e2e)`, () => {
}
});
// TODO remove with deprecated endpoint
describe('GET /asset/assetById/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/asset/assetById/${uuidStub.notFound}`);
expect(body).toEqual(errorStub.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${uuidStub.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${asset4.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.noPermission);
});
it('should get the asset info', async () => {
const { status, body } = await request(server)
.get(`/asset/assetById/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: asset1.id });
});
it('should work with a shared link', async () => {
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
});
const { status, body } = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: asset1.id });
});
it('should not send people data for shared links for un-authenticated users', async () => {
const personRepository = app.get<IPersonRepository>(IPersonRepository);
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
await personRepository.createFaces([
{
assetId: asset1.id,
personId: person.id,
embedding: Array.from({ length: 512 }, Math.random),
},
]);
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(status).toEqual(200);
expect(body).toMatchObject({
id: asset1.id,
isFavorite: true,
people: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '',
},
],
});
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
});
const data = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`);
expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] });
});
});
describe('GET /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`);
@@ -643,6 +558,47 @@ describe(`${AssetController.name} (e2e)`, () => {
expect(status).toBe(200);
expect(body).toMatchObject({ id: asset1.id });
});
it('should not send people data for shared links for un-authenticated users', async () => {
const personRepository = app.get<IPersonRepository>(IPersonRepository);
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
await personRepository.createFaces([
{
assetId: asset1.id,
personId: person.id,
embedding: Array.from({ length: 512 }, Math.random),
},
]);
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(status).toEqual(200);
expect(body).toMatchObject({
id: asset1.id,
isFavorite: true,
people: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '',
},
],
});
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
});
const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] });
});
});
describe('POST /asset/upload', () => {
@@ -920,46 +876,6 @@ describe(`${AssetController.name} (e2e)`, () => {
});
});
describe('POST /asset/download/info', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.post(`/asset/download/info`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should download info', async () => {
const { status, body } = await request(server)
.post('/asset/download/info')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(201);
expect(body).toEqual(expect.objectContaining({ archives: [expect.objectContaining({ assetIds: [asset1.id] })] }));
});
});
describe('POST /asset/download/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).post(`/asset/download/${asset1.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should download file', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example');
const response = await request(server)
.post(`/asset/download/${asset.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/jpeg');
});
});
describe('GET /asset/statistics', () => {
beforeEach(async () => {
await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true });
@@ -1459,20 +1375,20 @@ describe(`${AssetController.name} (e2e)`, () => {
});
});
const getAssetIdsWithoutFaces = async () => {
const assetPagination = usePagination(10, (pagination) =>
assetRepository.getWithout(pagination, WithoutProperty.FACES),
);
let assets: AssetEntity[] = [];
for await (const assetsPage of assetPagination) {
assets = [...assets, ...assetsPage];
}
return assets.map((a) => a.id);
};
describe(AssetRepository.name, () => {
describe('getWithout', () => {
describe('WithoutProperty.FACES', () => {
const getAssetIdsWithoutFaces = async () => {
const assetPagination = usePagination(10, (pagination) =>
assetRepository.getWithout(pagination, WithoutProperty.FACES),
);
let assets: AssetEntity[] = [];
for await (const assetsPage of assetPagination) {
assets = [...assets, ...assetsPage];
}
return assets.map((a) => a.id);
};
beforeEach(async () => {
await assetRepository.save({ id: asset1.id, resizePath: '/path/to/resize' });
expect(await getAssetIdsWithoutFaces()).toContain(asset1.id);
+89
View File
@@ -0,0 +1,89 @@
import { AssetResponseDto, IAssetRepository, LibraryResponseDto, LoginResponseDto, mapAsset } from '@app/domain';
import { AssetController } from '@app/immich';
import { AssetEntity } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { errorStub, userDto } from '@test/fixtures';
import request from 'supertest';
import { api } from '../../client';
import { generateAsset, testApp } from '../utils';
describe(`${AssetController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
let assetRepository: IAssetRepository;
let user1: LoginResponseDto;
let libraries: LibraryResponseDto[];
let asset1: AssetResponseDto;
const createAsset = async (
loginResponse: LoginResponseDto,
fileCreatedAt: Date,
other: Partial<AssetEntity> = {},
) => {
const asset = await assetRepository.create(
generateAsset(loginResponse.userId, libraries, { fileCreatedAt, ...other }),
);
return mapAsset(asset);
};
beforeAll(async () => {
app = await testApp.create();
server = app.getHttpServer();
assetRepository = app.get<IAssetRepository>(IAssetRepository);
await testApp.reset();
await api.authApi.adminSignUp(server);
const admin = await api.authApi.adminLogin(server);
await api.userApi.create(server, admin.accessToken, userDto.user1);
user1 = await api.authApi.login(server, userDto.user1);
libraries = await api.libraryApi.getAll(server, user1.accessToken);
asset1 = await createAsset(user1, new Date('1970-01-01'));
});
afterAll(async () => {
await testApp.teardown();
});
describe('POST /download/info', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.post(`/download/info`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should download info', async () => {
const { status, body } = await request(server)
.post('/download/info')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(201);
expect(body).toEqual(expect.objectContaining({ archives: [expect.objectContaining({ assetIds: [asset1.id] })] }));
});
});
describe('POST /download/asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).post(`/download/asset/${asset1.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should download file', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example');
const response = await request(server)
.post(`/download/asset/${asset.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/jpeg');
});
});
});
+2 -4
View File
@@ -1,7 +1,7 @@
import { AssetResponseDto } from '@app/domain';
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { randomBytes } from 'crypto';
import { randomBytes } from 'node:crypto';
import request from 'supertest';
type UploadDto = Partial<CreateAssetDto> & { content?: Buffer; filename?: string };
@@ -34,9 +34,7 @@ export const assetApi = {
return body as AssetResponseDto;
},
get: async (server: any, accessToken: string, id: string): Promise<AssetResponseDto> => {
const { body, status } = await request(server)
.get(`/asset/assetById/${id}`)
.set('Authorization', `Bearer ${accessToken}`);
const { body, status } = await request(server).get(`/asset/${id}`).set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
return body as AssetResponseDto;
},
-9
View File
@@ -106,15 +106,6 @@ export class OAuthConfigDto {
redirectUri!: string;
}
/** @deprecated use oauth authorize */
export class OAuthConfigResponseDto {
enabled!: boolean;
passwordLoginEnabled!: boolean;
url?: string;
buttonText?: string;
autoLaunch?: boolean;
}
export class OAuthAuthorizeResponseDto {
url!: string;
}
@@ -429,27 +429,6 @@ describe('AuthService', () => {
});
});
describe('generateConfig', () => {
it('should work when oauth is not configured', async () => {
configMock.load.mockResolvedValue(systemConfigStub.disabled);
await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({
enabled: false,
passwordLoginEnabled: false,
});
});
it('should generate the config', async () => {
configMock.load.mockResolvedValue(systemConfigStub.enabled);
await expect(sut.generateConfig({ redirectUri: 'http://redirect' })).resolves.toEqual({
enabled: true,
buttonText: 'OAuth',
url: 'http://authorization-url',
autoLaunch: false,
passwordLoginEnabled: true,
});
});
});
describe('callback', () => {
it('should throw an error if OAuth is not enabled', async () => {
await expect(sut.callback({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
-23
View File
@@ -42,7 +42,6 @@ import {
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
OAuthConfigResponseDto,
SignUpDto,
mapLoginResponse,
mapUserToken,
@@ -201,28 +200,6 @@ export class AuthService {
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
}
async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
const config = await this.configCore.getConfig();
const response = {
enabled: config.oauth.enabled,
passwordLoginEnabled: config.passwordLogin.enabled,
};
if (!response.enabled) {
return response;
}
const { scope, buttonText, autoLaunch } = config.oauth;
const oauthClient = await this.getOAuthClient(config);
const url = oauthClient.authorizationUrl({
redirect_uri: this.normalize(config, dto.redirectUri),
scope,
state: generators.state(),
});
return { ...response, buttonText, url, autoLaunch };
}
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
const config = await this.configCore.getConfig();
if (!config.oauth.enabled) {
@@ -230,8 +230,6 @@ export class SystemConfigCore {
[FeatureFlag.SIDECAR]: true,
[FeatureFlag.SEARCH]: true,
[FeatureFlag.TRASH]: config.trash.enabled,
// TODO: use these instead of `POST oauth/config`
[FeatureFlag.OAUTH]: config.oauth.enabled,
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
@@ -1,4 +1,4 @@
import { AssetResponseDto, AssetService, AuthDto } from '@app/domain';
import { AssetResponseDto, AuthDto } from '@app/domain';
import {
Body,
Controller,
@@ -18,7 +18,7 @@ import {
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../../app.guard';
import { UseValidation, sendFile } from '../../app.utils';
import { sendFile } from '../../app.utils';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
@@ -45,10 +45,7 @@ interface UploadFiles {
@Controller(Route.ASSET)
@Authenticated()
export class AssetController {
constructor(
private serviceV1: AssetServiceV1,
private service: AssetService,
) {}
constructor(private serviceV1: AssetServiceV1) {}
@SharedLinkRoute()
@Post('upload')
@@ -143,17 +140,6 @@ export class AssetController {
return this.serviceV1.getAllAssets(auth, dto);
}
/**
* Get a single asset's information
* @deprecated Use `/asset/:id`
*/
@SharedLinkRoute()
@UseValidation()
@Get('/assetById/:id')
getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.service.get(auth, id) as Promise<AssetResponseDto>;
}
/**
* Checks if multiple assets exist on the server and returns all existing - used by background backup
*/
@@ -1,7 +1,6 @@
import {
AssetBulkDeleteDto,
AssetBulkUpdateDto,
AssetIdsDto,
AssetJobsDto,
AssetResponseDto,
AssetSearchDto,
@@ -9,10 +8,7 @@ import {
AssetStatsDto,
AssetStatsResponseDto,
AuthDto,
BulkIdsDto,
DeviceIdDto,
DownloadInfoDto,
DownloadResponseDto,
DownloadService,
MapMarkerDto,
MapMarkerResponseDto,
@@ -26,25 +22,10 @@ import {
UpdateAssetDto as UpdateDto,
UpdateStackParentDto,
} from '@app/domain';
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Next,
Param,
Post,
Put,
Query,
Res,
StreamableFile,
} from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../app.guard';
import { UseValidation, asStreamableFile, sendFile } from '../app.utils';
import { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
import { Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@@ -87,42 +68,6 @@ export class AssetController {
return this.service.getRandom(auth, dto.count ?? 1);
}
/**
* @deprecated use `/download/info`
*/
@SharedLinkRoute()
@Post('download/info')
getDownloadInfoOld(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.downloadService.getDownloadInfo(auth, dto);
}
/**
* @deprecated use `/download/archive`
*/
@SharedLinkRoute()
@Post('download/archive')
@HttpCode(HttpStatus.OK)
@FileResponse()
downloadArchiveOld(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.downloadService.downloadArchive(auth, dto).then(asStreamableFile);
}
/**
* @deprecated use `/download/:id`
*/
@SharedLinkRoute()
@Post('download/:id')
@HttpCode(HttpStatus.OK)
@FileResponse()
async downloadFileOld(
@Res() res: Response,
@Next() next: NextFunction,
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
) {
await sendFile(res, next, () => this.downloadService.downloadFile(auth, id));
}
/**
* Get all asset of a device that are in the database, ID only.
*/
@@ -166,33 +111,6 @@ export class AssetController {
return this.service.deleteAll(auth, dto);
}
/**
* @deprecated use `POST /trash/restore/assets`
*/
@Post('restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreAssetsOld(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.trashService.restoreAssets(auth, dto);
}
/**
* @deprecated use `POST /trash/empty`
*/
@Post('trash/empty')
@HttpCode(HttpStatus.NO_CONTENT)
emptyTrashOld(@Auth() auth: AuthDto): Promise<void> {
return this.trashService.empty(auth);
}
/**
* @deprecated use `POST /trash/restore`
*/
@Post('trash/restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreTrashOld(@Auth() auth: AuthDto): Promise<void> {
return this.trashService.restore(auth);
}
@Put('stack/parent')
@HttpCode(HttpStatus.OK)
updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> {
@@ -6,7 +6,6 @@ import {
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
OAuthConfigResponseDto,
UserResponseDto,
} from '@app/domain';
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
@@ -32,13 +31,6 @@ export class OAuthController {
};
}
/** @deprecated use feature flags and /oauth/authorize */
@PublicRoute()
@Post('config')
generateOAuthConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
return this.service.generateConfig(dto);
}
@PublicRoute()
@Post('authorize')
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {