mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/search-filter-album/web
This commit is contained in:
@@ -19,6 +19,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { services } from 'src/services';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
@@ -55,6 +56,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
private jobService: JobService,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private authService: AuthService,
|
||||
private userRepository: UserRepository,
|
||||
) {
|
||||
logger.setAppName(this.worker);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ const create = (path: string, up: string[], down: string[]) => {
|
||||
const folder = dirname(path);
|
||||
const fullPath = join(folder, filename);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
|
||||
writeFileSync(fullPath, asMigration({ up, down }));
|
||||
console.log(`Wrote ${fullPath}`);
|
||||
};
|
||||
|
||||
@@ -128,34 +128,11 @@ const compare = async () => {
|
||||
};
|
||||
|
||||
type MigrationProps = {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
up: string[];
|
||||
down: string[];
|
||||
};
|
||||
|
||||
const asMigration = (type: 'kysely' | 'typeorm', options: MigrationProps) =>
|
||||
type === 'typeorm' ? asTypeOrmMigration(options) : asKyselyMigration(options);
|
||||
|
||||
const asTypeOrmMigration = ({ timestamp, name, up, down }: MigrationProps) => {
|
||||
const upSql = up.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
|
||||
const downSql = down.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
|
||||
|
||||
return `import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ${name}${timestamp} implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
${upSql}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
${downSql}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
const asKyselyMigration = ({ up, down }: MigrationProps) => {
|
||||
const asMigration = ({ up, down }: MigrationProps) => {
|
||||
const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
||||
const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { repositories } from 'src/repositories';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { getKyselyConfig } from 'src/utils/database';
|
||||
@@ -57,7 +58,7 @@ class SqlGenerator {
|
||||
try {
|
||||
await this.setup();
|
||||
for (const Repository of repositories) {
|
||||
if (Repository === LoggingRepository) {
|
||||
if (Repository === LoggingRepository || Repository === MachineLearningRepository) {
|
||||
continue;
|
||||
}
|
||||
await this.process(Repository);
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ChangeMediaLocationCommand extends CommandRunner {
|
||||
immich-server:
|
||||
...
|
||||
volumes:
|
||||
- \${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- \${UPLOAD_LOCATION}:/data
|
||||
...
|
||||
)`;
|
||||
|
||||
|
||||
+13
-1
@@ -54,6 +54,11 @@ export interface SystemConfig {
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
urls: string[];
|
||||
availabilityChecks: {
|
||||
enabled: boolean;
|
||||
timeout: number;
|
||||
interval: number;
|
||||
};
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
@@ -176,6 +181,8 @@ export interface SystemConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export type MachineLearningConfig = SystemConfig['machineLearning'];
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
backup: {
|
||||
database: {
|
||||
@@ -191,7 +198,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.Aac,
|
||||
acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le],
|
||||
acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus],
|
||||
acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
@@ -227,6 +234,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'],
|
||||
availabilityChecks: {
|
||||
enabled: true,
|
||||
timeout: Number(process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT) || 2000,
|
||||
interval: 30_000,
|
||||
},
|
||||
clip: {
|
||||
enabled: true,
|
||||
modelName: 'ViT-B-32__openai',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Duration } from 'luxon';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { SemVer } from 'semver';
|
||||
import { DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum';
|
||||
|
||||
export const POSTGRES_VERSION_RANGE = '>=14.0.0';
|
||||
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.5';
|
||||
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.6';
|
||||
export const VECTORS_VERSION_RANGE = '>=0.2 <0.4';
|
||||
export const VECTOR_VERSION_RANGE = '>=0.5 <1';
|
||||
|
||||
@@ -41,20 +42,17 @@ export const SALT_ROUNDS = 10;
|
||||
|
||||
export const IWorker = 'IWorker';
|
||||
|
||||
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const basePath = dirname(__filename);
|
||||
const packageFile = join(basePath, '..', 'package.json');
|
||||
const { version } = JSON.parse(readFileSync(packageFile, 'utf8'));
|
||||
export const serverVersion = new SemVer(version);
|
||||
|
||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||
|
||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || '/usr/src/app/upload';
|
||||
|
||||
export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000);
|
||||
export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number(
|
||||
process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000,
|
||||
);
|
||||
|
||||
export const citiesFile = 'cities500.txt';
|
||||
export const reverseGeocodeMaxDistance = 25_000;
|
||||
|
||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
|
||||
@@ -46,8 +46,8 @@ export class ActivityController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.ActivityDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,13 @@ describe(AlbumController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /albums/assets', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/albums/assets`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /albums/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' });
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
AddUsersDto,
|
||||
AlbumInfoDto,
|
||||
AlbumResponseDto,
|
||||
AlbumsAddAssetsDto,
|
||||
AlbumsAddAssetsResponseDto,
|
||||
AlbumStatisticsResponseDto,
|
||||
CreateAlbumDto,
|
||||
GetAlbumsDto,
|
||||
@@ -68,12 +70,13 @@ export class AlbumController {
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.AlbumDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/assets')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true })
|
||||
addAssetsToAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -82,8 +85,14 @@ export class AlbumController {
|
||||
return this.service.addAssets(auth, id, dto);
|
||||
}
|
||||
|
||||
@Put('assets')
|
||||
@Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true })
|
||||
addAssetsToAlbums(@Auth() auth: AuthDto, @Body() dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> {
|
||||
return this.service.addAssetsToAlbums(auth, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/assets')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AlbumAssetDelete })
|
||||
removeAssetFromAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: BulkIdsDto,
|
||||
@@ -93,7 +102,7 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Put(':id/users')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AlbumUserCreate })
|
||||
addUsersToAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -103,7 +112,8 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Put(':id/user/:userId')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AlbumUserUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
updateAlbumUser(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -114,12 +124,13 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Delete(':id/user/:userId')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AlbumUserDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeUserFromAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
|
||||
) {
|
||||
): Promise<void> {
|
||||
return this.service.removeUser(auth, id, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { ApiKeyController } from 'src/controllers/api-key.controller';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ApiKeyService } from 'src/services/api-key.service';
|
||||
import request from 'supertest';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(APIKeyController.name, () => {
|
||||
describe(ApiKeyController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(ApiKeyService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(APIKeyController, [{ provide: ApiKeyService, useValue: service }]);
|
||||
ctx = await controllerSetup(ApiKeyController, [{ provide: ApiKeyService, useValue: service }]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
@@ -33,6 +33,13 @@ describe(APIKeyController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api-keys/me', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/api-keys/me`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api-keys/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('API Keys')
|
||||
@Controller('api-keys')
|
||||
export class APIKeyController {
|
||||
export class ApiKeyController {
|
||||
constructor(private service: ApiKeyService) {}
|
||||
|
||||
@Post()
|
||||
@@ -24,6 +24,12 @@ export class APIKeyController {
|
||||
return this.service.getAll(auth);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@Authenticated({ permission: false })
|
||||
async getMyApiKey(@Auth() auth: AuthDto): Promise<APIKeyResponseDto> {
|
||||
return this.service.getMine(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.ApiKeyRead })
|
||||
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
|
||||
@@ -41,8 +47,8 @@ export class APIKeyController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.ApiKeyDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
||||
import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMetadataKey } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import request from 'supertest';
|
||||
@@ -11,7 +13,7 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||
deviceId: 'TEST',
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
fileModifiedAt: new Date().toISOString(),
|
||||
isFavorite: 'testing',
|
||||
isFavorite: 'false',
|
||||
duration: '0:00:00.000000',
|
||||
};
|
||||
|
||||
@@ -27,16 +29,20 @@ describe(AssetMediaController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const assetData = Buffer.from('123');
|
||||
const filename = 'example.png';
|
||||
const service = mockBaseService(AssetMediaService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(AssetMediaController, [
|
||||
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
||||
{ provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) },
|
||||
{ provide: AssetMediaService, useValue: service },
|
||||
]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.resetAllMocks();
|
||||
service.uploadAsset.mockResolvedValue({ status: AssetMediaStatus.DUPLICATE, id: factory.uuid() });
|
||||
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
@@ -46,13 +52,61 @@ describe(AssetMediaController.name, () => {
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept metadata', async () => {
|
||||
const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } };
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({
|
||||
...makeUploadDto(),
|
||||
metadata: JSON.stringify([mobileMetadata]),
|
||||
});
|
||||
|
||||
expect(service.uploadAsset).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ metadata: [mobileMetadata] }),
|
||||
expect.objectContaining({ originalName: 'example.png' }),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should handle invalid metadata json', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({
|
||||
...makeUploadDto(),
|
||||
metadata: 'not-a-string-string',
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON']));
|
||||
});
|
||||
|
||||
it('should validate iCloudId is a string', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({
|
||||
...makeUploadDto(),
|
||||
metadata: JSON.stringify([{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 123 } }]),
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudId must be a string']));
|
||||
});
|
||||
|
||||
it('should require `deviceAssetId`', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto({ omit: 'deviceAssetId' }) });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['deviceAssetId must be a string', 'deviceAssetId should not be empty']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require `deviceId`', async () => {
|
||||
@@ -61,7 +115,7 @@ describe(AssetMediaController.name, () => {
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto({ omit: 'deviceId' }) });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(factory.responses.badRequest(['deviceId must be a string', 'deviceId should not be empty']));
|
||||
});
|
||||
|
||||
it('should require `fileCreatedAt`', async () => {
|
||||
@@ -70,25 +124,20 @@ describe(AssetMediaController.name, () => {
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['fileCreatedAt must be a Date instance', 'fileCreatedAt should not be empty']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require `fileModifiedAt`', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) });
|
||||
.field(makeUploadDto({ omit: 'fileModifiedAt' }));
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
});
|
||||
|
||||
it('should require `duration`', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto({ omit: 'duration' }) });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(['fileModifiedAt must be a Date instance', 'fileModifiedAt should not be empty']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if `isFavorite` is not a boolean', async () => {
|
||||
@@ -97,16 +146,18 @@ describe(AssetMediaController.name, () => {
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(factory.responses.badRequest(['isFavorite must be a boolean value']));
|
||||
});
|
||||
|
||||
it('should throw if `visibility` is not an enum', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/assets')
|
||||
.attach('assetData', assetData, filename)
|
||||
.field({ ...makeUploadDto(), visibility: 'not-a-boolean' });
|
||||
.field({ ...makeUploadDto(), visibility: 'not-an-option' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest([expect.stringContaining('visibility must be one of the following values:')]),
|
||||
);
|
||||
});
|
||||
|
||||
// TODO figure out how to deal with `sendFile`
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ImmichHeader, RouteKey } from 'src/enum';
|
||||
import { ImmichHeader, Permission, RouteKey } from 'src/enum';
|
||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||
@@ -61,7 +61,7 @@ export class AssetMediaController {
|
||||
required: false,
|
||||
})
|
||||
@ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto })
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetUpload, sharedLink: true })
|
||||
async uploadAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
|
||||
@@ -80,7 +80,7 @@ export class AssetMediaController {
|
||||
|
||||
@Get(':id/original')
|
||||
@FileResponse()
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
|
||||
async downloadAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -96,12 +96,13 @@ export class AssetMediaController {
|
||||
@Put(':id/original')
|
||||
@UseInterceptors(FileUploadInterceptor)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@EndpointLifecycle({ addedAt: 'v1.106.0' })
|
||||
@ApiOperation({
|
||||
@EndpointLifecycle({
|
||||
addedAt: 'v1.106.0',
|
||||
deprecatedAt: 'v1.142.0',
|
||||
summary: 'replaceAsset',
|
||||
description: 'Replace the asset with new file, without changing its id',
|
||||
})
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetReplace, sharedLink: true })
|
||||
async replaceAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -120,7 +121,7 @@ export class AssetMediaController {
|
||||
|
||||
@Get(':id/thumbnail')
|
||||
@FileResponse()
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||
async viewAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -157,7 +158,7 @@ export class AssetMediaController {
|
||||
|
||||
@Get(':id/video/playback')
|
||||
@FileResponse()
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||
async playAssetVideo(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -171,12 +172,12 @@ export class AssetMediaController {
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('exist')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@ApiOperation({
|
||||
summary: 'checkExistingAssets',
|
||||
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
|
||||
})
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
checkExistingAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CheckExistingAssetsDto,
|
||||
@@ -188,12 +189,12 @@ export class AssetMediaController {
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Post('bulk-upload-check')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.AssetUpload })
|
||||
@ApiOperation({
|
||||
summary: 'checkBulkUpload',
|
||||
description: 'Checks if assets exist by checksums',
|
||||
})
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
checkBulkUpload(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: AssetBulkUploadCheckDto,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { AssetMetadataKey } from 'src/enum';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import request from 'supertest';
|
||||
import { factory } from 'test/small.factory';
|
||||
@@ -6,14 +7,16 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
|
||||
|
||||
describe(AssetController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(AssetService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]);
|
||||
ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: service }]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.reset();
|
||||
service.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('PUT /assets', () => {
|
||||
@@ -115,4 +118,120 @@ describe(AssetController.name, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/:id/metadata', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /assets/:id/metadata', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({ items: [] });
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should require items to be an array', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['items must be an array']));
|
||||
});
|
||||
|
||||
it('should require each item to have a valid key', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/assets/${factory.uuid()}/metadata`)
|
||||
.send({ items: [{ key: 'someKey' }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining([expect.stringContaining('items.0.key must be one of the following values')]),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should require each item to have a value', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/assets/${factory.uuid()}/metadata`)
|
||||
.send({ items: [{ key: 'mobile-app', value: null }] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(expect.arrayContaining([expect.stringContaining('value must be an object')])),
|
||||
);
|
||||
});
|
||||
|
||||
describe(AssetMetadataKey.MobileApp, () => {
|
||||
it('should accept valid data and pass to service correctly', async () => {
|
||||
const assetId = factory.uuid();
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.put(`/assets/${assetId}/metadata`)
|
||||
.send({ items: [{ key: 'mobile-app', value: { iCloudId: '123' } }] });
|
||||
expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, {
|
||||
items: [{ key: 'mobile-app', value: { iCloudId: '123' } }],
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should work without iCloudId', async () => {
|
||||
const assetId = factory.uuid();
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.put(`/assets/${assetId}/metadata`)
|
||||
.send({ items: [{ key: 'mobile-app', value: {} }] });
|
||||
expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, {
|
||||
items: [{ key: 'mobile-app', value: {} }],
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/:id/metadata/:key', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/mobile-app`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should require a valid key', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest(
|
||||
expect.arrayContaining([expect.stringContaining('key must be one of the following value')]),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /assets/:id/metadata/:key', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require a valid key', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.badRequest([expect.stringContaining('key must be one of the following values')]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
AssetBulkDeleteDto,
|
||||
AssetBulkUpdateDto,
|
||||
AssetJobsDto,
|
||||
AssetMetadataResponseDto,
|
||||
AssetMetadataRouteParams,
|
||||
AssetMetadataUpsertDto,
|
||||
AssetStatsDto,
|
||||
AssetStatsResponseDto,
|
||||
DeviceIdDto,
|
||||
@@ -13,7 +16,7 @@ import {
|
||||
UpdateAssetDto,
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { RouteKey } from 'src/enum';
|
||||
import { Permission, RouteKey } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@@ -24,7 +27,7 @@ export class AssetController {
|
||||
constructor(private service: AssetService) {}
|
||||
|
||||
@Get('random')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@EndpointLifecycle({ deprecatedAt: 'v1.116.0' })
|
||||
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getRandom(auth, dto.count ?? 1);
|
||||
@@ -44,7 +47,7 @@ export class AssetController {
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetStatistics })
|
||||
getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
|
||||
return this.service.getStatistics(auth, dto);
|
||||
}
|
||||
@@ -57,27 +60,27 @@ export class AssetController {
|
||||
}
|
||||
|
||||
@Put()
|
||||
@Authenticated({ permission: Permission.AssetUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@Authenticated({ permission: Permission.AssetDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetRead, sharedLink: true })
|
||||
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
|
||||
return this.service.get(auth, id) as Promise<AssetResponseDto>;
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetUpdate })
|
||||
updateAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -85,4 +88,36 @@ export class AssetController {
|
||||
): Promise<AssetResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/metadata')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> {
|
||||
return this.service.getMetadata(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/metadata')
|
||||
@Authenticated({ permission: Permission.AssetUpdate })
|
||||
updateAssetMetadata(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: AssetMetadataUpsertDto,
|
||||
): Promise<AssetMetadataResponseDto[]> {
|
||||
return this.service.upsertMetadata(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/metadata/:key')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getAssetMetadataByKey(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id, key }: AssetMetadataRouteParams,
|
||||
): Promise<AssetMetadataResponseDto> {
|
||||
return this.service.getMetadataByKey(auth, id, key);
|
||||
}
|
||||
|
||||
@Delete(':id/metadata/:key')
|
||||
@Authenticated({ permission: Permission.AssetUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> {
|
||||
return this.service.deleteMetadataByKey(auth, id, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AuthAdminService } from 'src/services/auth-admin.service';
|
||||
|
||||
@ApiTags('Auth (admin)')
|
||||
@Controller('admin/auth')
|
||||
export class AuthAdminController {
|
||||
constructor(private service: AuthAdminService) {}
|
||||
@Post('unlink-all')
|
||||
@Authenticated({ permission: Permission.AdminAuthUnlinkAll, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
unlinkAllOAuthAccountsAdmin(@Auth() auth: AuthDto): Promise<void> {
|
||||
return this.service.unlinkAll(auth);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ValidateAccessTokenResponseDto,
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||
import { AuthType, ImmichCookie } from 'src/enum';
|
||||
import { AuthType, ImmichCookie, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
||||
@@ -49,22 +49,22 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('validateToken')
|
||||
@Authenticated({ permission: false })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
validateAccessToken(): ValidateAccessTokenResponseDto {
|
||||
return { authStatus: true };
|
||||
}
|
||||
|
||||
@Post('change-password')
|
||||
@Authenticated({ permission: Permission.AuthChangePassword })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.changePassword(auth, dto);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async logout(
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@@ -87,33 +87,36 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('pin-code')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PinCodeCreate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> {
|
||||
return this.service.setupPinCode(auth, dto);
|
||||
}
|
||||
|
||||
@Put('pin-code')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PinCodeUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
|
||||
return this.service.changePinCode(auth, dto);
|
||||
}
|
||||
|
||||
@Delete('pin-code')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PinCodeDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
|
||||
return this.service.resetPinCode(auth, dto);
|
||||
}
|
||||
|
||||
@Post('session/unlock')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
|
||||
return this.service.unlockSession(auth, dto);
|
||||
}
|
||||
|
||||
@Post('session/lock')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
|
||||
return this.service.lockSession(auth);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Readable } from 'node:stream';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import request from 'supertest';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
import { Readable } from 'typeorm/platform/PlatformTools.js';
|
||||
|
||||
describe(DownloadController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import { asStreamableFile } from 'src/utils/file';
|
||||
@@ -13,15 +14,15 @@ export class DownloadController {
|
||||
constructor(private service: DownloadService) {}
|
||||
|
||||
@Post('info')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
|
||||
getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
|
||||
return this.service.getDownloadInfo(auth, dto);
|
||||
}
|
||||
|
||||
@Post('archive')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
|
||||
@FileResponse()
|
||||
@Authenticated({ sharedLink: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
|
||||
return this.service.downloadArchive(auth, dto).then(asStreamableFile);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@@ -13,19 +14,21 @@ export class DuplicateController {
|
||||
constructor(private service: DuplicateService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.DuplicateRead })
|
||||
getAssetDuplicates(@Auth() auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||
return this.service.getDuplicates(auth);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.DuplicateDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.DuplicateDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
@@ -42,7 +42,8 @@ export class FaceController {
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.FaceDelete })
|
||||
deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto) {
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto): Promise<void> {
|
||||
return this.service.deleteFace(auth, id, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ActivityController } from 'src/controllers/activity.controller';
|
||||
import { AlbumController } from 'src/controllers/album.controller';
|
||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { ApiKeyController } from 'src/controllers/api-key.controller';
|
||||
import { AppController } from 'src/controllers/app.controller';
|
||||
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { AuthAdminController } from 'src/controllers/auth-admin.controller';
|
||||
import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
@@ -33,13 +34,14 @@ import { UserController } from 'src/controllers/user.controller';
|
||||
import { ViewController } from 'src/controllers/view.controller';
|
||||
|
||||
export const controllers = [
|
||||
APIKeyController,
|
||||
ApiKeyController,
|
||||
ActivityController,
|
||||
AlbumController,
|
||||
AppController,
|
||||
AssetController,
|
||||
AssetMediaController,
|
||||
AuthController,
|
||||
AuthAdminController,
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
FaceController,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
|
||||
@@ -10,19 +11,20 @@ export class JobController {
|
||||
constructor(private service: JobService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.JobRead, admin: true })
|
||||
getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
|
||||
return this.service.getAllJobsStatus();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.JobCreate, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
createJob(@Body() dto: JobCreateDto): Promise<void> {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.JobCreate, admin: true })
|
||||
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
|
||||
return this.service.handleCommand(id, dto);
|
||||
}
|
||||
|
||||
@@ -43,15 +43,15 @@ export class LibraryController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.LibraryDelete, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
|
||||
@Post(':id/validate')
|
||||
@HttpCode(200)
|
||||
@Authenticated({ admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
// TODO: change endpoint to validate current settings instead
|
||||
validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
||||
return this.service.validate(id, dto);
|
||||
@@ -64,9 +64,9 @@ export class LibraryController {
|
||||
}
|
||||
|
||||
@Post(':id/scan')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.LibraryUpdate, admin: true })
|
||||
scanLibrary(@Param() { id }: UUIDParamDto) {
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
scanLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.queueScan(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class MemoryController {
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@Authenticated({ permission: Permission.MemoryRead })
|
||||
@Authenticated({ permission: Permission.MemoryStatistics })
|
||||
memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryStatisticsResponseDto> {
|
||||
return this.service.statistics(auth, dto);
|
||||
}
|
||||
@@ -54,14 +54,14 @@ export class MemoryController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.MemoryDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/assets')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MemoryAssetCreate })
|
||||
addMemoryAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@@ -71,8 +71,8 @@ export class MemoryController {
|
||||
}
|
||||
|
||||
@Delete(':id/assets')
|
||||
@Authenticated({ permission: Permission.MemoryAssetDelete })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
removeMemoryAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: BulkIdsDto,
|
||||
|
||||
@@ -25,15 +25,15 @@ export class NotificationAdminController {
|
||||
}
|
||||
|
||||
@Post('test-email')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
sendTestEmailAdmin(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> {
|
||||
return this.service.sendTestEmail(auth.user.id, dto);
|
||||
}
|
||||
|
||||
@Post('templates/:name')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
getNotificationTemplateAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param('name') name: EmailTemplate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
@@ -26,12 +26,14 @@ export class NotificationController {
|
||||
|
||||
@Put()
|
||||
@Authenticated({ permission: Permission.NotificationUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@Authenticated({ permission: Permission.NotificationDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
@@ -54,6 +56,7 @@ export class NotificationController {
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.NotificationDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export class OAuthController {
|
||||
|
||||
@Post('link')
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
linkOAuthAccount(
|
||||
@Req() request: Request,
|
||||
@Auth() auth: AuthDto,
|
||||
@@ -79,8 +80,8 @@ export class OAuthController {
|
||||
}
|
||||
|
||||
@Post('unlink')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.unlink(auth);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import request from 'supertest';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(PartnerController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(PartnerService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(PartnerController, [
|
||||
{ provide: PartnerService, useValue: service },
|
||||
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
||||
]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.resetAllMocks();
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
describe('GET /partners', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get('/partners');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should require a direction`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'direction should not be empty',
|
||||
expect.stringContaining('direction must be one of the following values:'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should require direction to be an enum`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.get(`/partners`)
|
||||
.query({ direction: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('direction must be one of the following values:')]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /partners', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/partners');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should require sharedWithId to be a uuid`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post(`/partners`)
|
||||
.send({ sharedWithId: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /partners/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/partners/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should require id to be a uuid`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/partners/invalid`)
|
||||
.send({ inTimeline: true })
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /partners/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/partners/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should require id to be a uuid`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.delete(`/partners/invalid`)
|
||||
.set('Authorization', `Bearer token`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { EndpointLifecycle } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
@@ -18,10 +19,17 @@ export class PartnerController {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.PartnerCreate })
|
||||
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
|
||||
return this.service.create(auth, id);
|
||||
createPartner(@Auth() auth: AuthDto, @Body() dto: PartnerCreateDto): Promise<PartnerResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
@EndpointLifecycle({ deprecatedAt: 'v1.141.0' })
|
||||
@Authenticated({ permission: Permission.PartnerCreate })
|
||||
createPartnerDeprecated(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
|
||||
return this.service.create(auth, { sharedWithId: id });
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@@ -29,13 +37,14 @@ export class PartnerController {
|
||||
updatePartner(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: UpdatePartnerDto,
|
||||
@Body() dto: PartnerUpdateDto,
|
||||
): Promise<PartnerResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.PartnerDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ export class PersonController {
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PersonDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
@@ -86,8 +86,8 @@ export class PersonController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PersonDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
@@ -122,6 +122,7 @@ export class PersonController {
|
||||
|
||||
@Post(':id/merge')
|
||||
@Authenticated({ permission: Permission.PersonMerge })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
mergePerson(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
|
||||
@@ -128,12 +128,6 @@ describe(SearchController.name, () => {
|
||||
await request(ctx.getHttpServer()).post('/search/smart');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a query', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /search/explore', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import {
|
||||
LargeAssetSearchDto,
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
SmartSearchDto,
|
||||
StatisticsSearchDto,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
|
||||
@@ -25,59 +27,66 @@ export class SearchController {
|
||||
constructor(private service: SearchService) {}
|
||||
|
||||
@Post('metadata')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchMetadata(auth, dto);
|
||||
}
|
||||
|
||||
@Post('statistics')
|
||||
@Authenticated({ permission: Permission.AssetStatistics })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
|
||||
return this.service.searchStatistics(auth, dto);
|
||||
}
|
||||
|
||||
@Post('random')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.searchRandom(auth, dto);
|
||||
}
|
||||
|
||||
@Post('smart')
|
||||
@Post('large-assets')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
searchLargeAssets(@Auth() auth: AuthDto, @Query() dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.searchLargeAssets(auth, dto);
|
||||
}
|
||||
|
||||
@Post('smart')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchSmart(auth, dto);
|
||||
}
|
||||
|
||||
@Get('explore')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
|
||||
return this.service.getExploreData(auth);
|
||||
}
|
||||
|
||||
@Get('person')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PersonRead })
|
||||
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||
return this.service.searchPerson(auth, dto);
|
||||
}
|
||||
|
||||
@Get('places')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
|
||||
return this.service.searchPlaces(dto);
|
||||
}
|
||||
|
||||
@Get('cities')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getAssetsByCity(auth);
|
||||
}
|
||||
|
||||
@Get('suggestions')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
|
||||
// TODO fix open api generation to indicate that results can be nullable
|
||||
return this.service.getSearchSuggestions(auth, dto) as Promise<string[]>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Put } from '@nestjs/common';
|
||||
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ServerVersionResponseDto,
|
||||
} from 'src/dtos/server.dto';
|
||||
import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||
@@ -30,19 +31,19 @@ export class ServerController {
|
||||
) {}
|
||||
|
||||
@Get('about')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ServerAbout })
|
||||
getAboutInfo(): Promise<ServerAboutResponseDto> {
|
||||
return this.service.getAboutInfo();
|
||||
}
|
||||
|
||||
@Get('apk-links')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ServerApkLinks })
|
||||
getApkLinks(): ServerApkLinksDto {
|
||||
return this.service.getApkLinks();
|
||||
}
|
||||
|
||||
@Get('storage')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ServerStorage })
|
||||
getStorage(): Promise<ServerStorageResponseDto> {
|
||||
return this.service.getStorage();
|
||||
}
|
||||
@@ -78,7 +79,7 @@ export class ServerController {
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ServerStatistics, admin: true })
|
||||
getServerStatistics(): Promise<ServerStatsResponseDto> {
|
||||
return this.service.getStatistics();
|
||||
}
|
||||
@@ -88,27 +89,28 @@ export class ServerController {
|
||||
return this.service.getSupportedMediaTypes();
|
||||
}
|
||||
|
||||
@Put('license')
|
||||
@Authenticated({ admin: true })
|
||||
setServerLicense(@Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
return this.service.setLicense(license);
|
||||
}
|
||||
|
||||
@Delete('license')
|
||||
@Authenticated({ admin: true })
|
||||
deleteServerLicense(): Promise<void> {
|
||||
return this.service.deleteLicense();
|
||||
}
|
||||
|
||||
@Get('license')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ServerLicenseRead, admin: true })
|
||||
@ApiNotFoundResponse()
|
||||
getServerLicense(): Promise<LicenseResponseDto> {
|
||||
return this.service.getLicense();
|
||||
}
|
||||
|
||||
@Put('license')
|
||||
@Authenticated({ permission: Permission.ServerLicenseUpdate, admin: true })
|
||||
setServerLicense(@Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
return this.service.setLicense(license);
|
||||
}
|
||||
|
||||
@Delete('license')
|
||||
@Authenticated({ permission: Permission.ServerLicenseDelete, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteServerLicense(): Promise<void> {
|
||||
return this.service.deleteLicense();
|
||||
}
|
||||
|
||||
@Get('version-check')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ServerVersionCheck })
|
||||
getVersionCheck(): Promise<VersionCheckStateResponseDto> {
|
||||
return this.systemMetadataService.getVersionCheckState();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
@@ -73,6 +87,7 @@ export class SharedLinkController {
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.SharedLinkDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { StackService } from 'src/services/stack.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
import { UUIDAssetIDParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Stacks')
|
||||
@Controller('stacks')
|
||||
@@ -49,9 +49,16 @@ export class StackController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.StackDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Delete(':id/assets/:assetId')
|
||||
@Authenticated({ permission: Permission.StackUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeAssetFromStack(@Auth() auth: AuthDto, @Param() dto: UUIDAssetIDParamDto): Promise<void> {
|
||||
return this.service.removeAsset(auth, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SyncAckSetDto,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
@@ -25,23 +26,23 @@ export class SyncController {
|
||||
) {}
|
||||
|
||||
@Post('full-sync')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getFullSync(auth, dto);
|
||||
}
|
||||
|
||||
@Post('delta-sync')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
|
||||
return this.service.getDeltaSync(auth, dto);
|
||||
}
|
||||
|
||||
@Post('stream')
|
||||
@Authenticated({ permission: Permission.SyncStream })
|
||||
@Header('Content-Type', 'application/jsonlines+json')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
async getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) {
|
||||
try {
|
||||
await this.service.stream(auth, res, dto);
|
||||
@@ -52,22 +53,22 @@ export class SyncController {
|
||||
}
|
||||
|
||||
@Get('ack')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SyncCheckpointRead })
|
||||
getSyncAck(@Auth() auth: AuthDto): Promise<SyncAckDto[]> {
|
||||
return this.service.getAcks(auth);
|
||||
}
|
||||
|
||||
@Post('ack')
|
||||
@Authenticated({ permission: Permission.SyncCheckpointUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
sendSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckSetDto) {
|
||||
return this.service.setAcks(auth, dto);
|
||||
}
|
||||
|
||||
@Delete('ack')
|
||||
@Authenticated({ permission: Permission.SyncCheckpointDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto) {
|
||||
deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto): Promise<void> {
|
||||
return this.service.deleteAcks(auth, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ export class SystemMetadataController {
|
||||
}
|
||||
|
||||
@Post('admin-onboarding')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
|
||||
return this.service.updateAdminOnboarding(dto);
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ export class TagController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.TagDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
||||
@@ -13,22 +13,22 @@ export class TrashController {
|
||||
constructor(private service: TrashService) {}
|
||||
|
||||
@Post('empty')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.AssetDelete })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
|
||||
return this.service.empty(auth);
|
||||
}
|
||||
|
||||
@Post('restore')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.AssetDelete })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
|
||||
return this.service.restore(auth);
|
||||
}
|
||||
|
||||
@Post('restore/assets')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.AssetDelete })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
|
||||
return this.service.restoreAssets(auth, dto);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { UserAdminController } from 'src/controllers/user-admin.controller';
|
||||
import { UserAdminCreateDto } from 'src/dtos/user.dto';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { UserAdminService } from 'src/services/user-admin.service';
|
||||
import request from 'supertest';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(UserAdminController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(UserAdminService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(UserAdminController, [
|
||||
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
||||
{ provide: UserAdminService, useValue: service },
|
||||
]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.resetAllMocks();
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
describe('GET /admin/users', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get('/admin/users');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /admin/users', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/admin/users');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not allow decimal quota`, async () => {
|
||||
const dto: UserAdminCreateDto = {
|
||||
email: 'user@immich.app',
|
||||
password: 'test',
|
||||
name: 'Test User',
|
||||
quotaSizeInBytes: 1.2,
|
||||
};
|
||||
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post(`/admin/users`)
|
||||
.set('Authorization', `Bearer token`)
|
||||
.send(dto);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /admin/users/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/admin/users/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /admin/users/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not allow decimal quota`, async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/admin/users/${factory.uuid()}`)
|
||||
.set('Authorization', `Bearer token`)
|
||||
.send({ quotaSizeInBytes: 1.2 });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||
import { RouteKey } from 'src/enum';
|
||||
import { Permission, RouteKey } from 'src/enum';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -38,31 +38,31 @@ export class UserController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
getMyUser(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.getMe(auth);
|
||||
}
|
||||
|
||||
@Put('me')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserUpdate })
|
||||
updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.updateMe(auth, dto);
|
||||
}
|
||||
|
||||
@Get('me/preferences')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserPreferenceRead })
|
||||
getMyPreferences(@Auth() auth: AuthDto): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.getMyPreferences(auth);
|
||||
}
|
||||
|
||||
@Put('me/preferences')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserPreferenceUpdate })
|
||||
updateMyPreferences(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: UserPreferencesUpdateDto,
|
||||
@@ -71,52 +71,54 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Get('me/license')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserLicenseRead })
|
||||
getUserLicense(@Auth() auth: AuthDto): Promise<LicenseResponseDto> {
|
||||
return this.service.getLicense(auth);
|
||||
}
|
||||
|
||||
@Put('me/license')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserLicenseUpdate })
|
||||
async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
return this.service.setLicense(auth, license);
|
||||
}
|
||||
|
||||
@Delete('me/license')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserLicenseDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteUserLicense(@Auth() auth: AuthDto): Promise<void> {
|
||||
await this.service.deleteLicense(auth);
|
||||
}
|
||||
|
||||
@Get('me/onboarding')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserOnboardingRead })
|
||||
getUserOnboarding(@Auth() auth: AuthDto): Promise<OnboardingResponseDto> {
|
||||
return this.service.getOnboarding(auth);
|
||||
}
|
||||
|
||||
@Put('me/onboarding')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserOnboardingUpdate })
|
||||
async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise<OnboardingResponseDto> {
|
||||
return this.service.setOnboarding(auth, Onboarding);
|
||||
}
|
||||
|
||||
@Delete('me/onboarding')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserOnboardingDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteUserOnboarding(@Auth() auth: AuthDto): Promise<void> {
|
||||
await this.service.deleteOnboarding(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
return this.service.get(id);
|
||||
}
|
||||
|
||||
@Post('profile-image')
|
||||
@Authenticated({ permission: Permission.UserProfileImageUpdate })
|
||||
@UseInterceptors(FileUploadInterceptor)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
|
||||
@Post('profile-image')
|
||||
@Authenticated()
|
||||
createProfileImage(
|
||||
@Auth() auth: AuthDto,
|
||||
@UploadedFile() fileInfo: Express.Multer.File,
|
||||
@@ -125,15 +127,15 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Delete('profile-image')
|
||||
@Authenticated({ permission: Permission.UserProfileImageDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
|
||||
return this.service.deleteProfileImage(auth);
|
||||
}
|
||||
|
||||
@Get(':id/profile-image')
|
||||
@FileResponse()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.UserProfileImageRead })
|
||||
async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) {
|
||||
await sendFile(res, next, () => this.service.getProfileImage(id), this.logger);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { StorageCore } from 'src/cores/storage.core';
|
||||
import { vitest } from 'vitest';
|
||||
|
||||
vitest.mock('src/constants', () => ({
|
||||
APP_MEDIA_LOCATION: '/photos',
|
||||
ADDED_IN_PREFIX: 'This property was added in ',
|
||||
DEPRECATED_IN_PREFIX: 'This property was deprecated in ',
|
||||
IWorker: 'IWorker',
|
||||
@@ -10,6 +9,10 @@ vitest.mock('src/constants', () => ({
|
||||
|
||||
describe('StorageCore', () => {
|
||||
describe('isImmichPath', () => {
|
||||
beforeAll(() => {
|
||||
StorageCore.setMediaLocation('/photos');
|
||||
});
|
||||
|
||||
it('should return true for APP_MEDIA_LOCATION path', () => {
|
||||
const immichPath = '/photos';
|
||||
expect(StorageCore.isImmichPath(immichPath)).toBe(true);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||
import { StorageAsset } from 'src/database';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
@@ -32,6 +31,8 @@ export type ThumbnailPathEntity = { id: string; ownerId: string };
|
||||
|
||||
let instance: StorageCore | null;
|
||||
|
||||
let mediaLocation: string | undefined;
|
||||
|
||||
export class StorageCore {
|
||||
private constructor(
|
||||
private assetRepository: AssetRepository,
|
||||
@@ -42,7 +43,9 @@ export class StorageCore {
|
||||
private storageRepository: StorageRepository,
|
||||
private systemMetadataRepository: SystemMetadataRepository,
|
||||
private logger: LoggingRepository,
|
||||
) {}
|
||||
) {
|
||||
this.logger.setContext(StorageCore.name);
|
||||
}
|
||||
|
||||
static create(
|
||||
assetRepository: AssetRepository,
|
||||
@@ -74,6 +77,18 @@ export class StorageCore {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
static getMediaLocation(): string {
|
||||
if (mediaLocation === undefined) {
|
||||
throw new Error('Media location is not set.');
|
||||
}
|
||||
|
||||
return mediaLocation;
|
||||
}
|
||||
|
||||
static setMediaLocation(location: string) {
|
||||
mediaLocation = location;
|
||||
}
|
||||
|
||||
static getFolderLocation(folder: StorageFolder, userId: string) {
|
||||
return join(StorageCore.getBaseFolder(folder), userId);
|
||||
}
|
||||
@@ -83,7 +98,7 @@ export class StorageCore {
|
||||
}
|
||||
|
||||
static getBaseFolder(folder: StorageFolder) {
|
||||
return join(APP_MEDIA_LOCATION, folder);
|
||||
return join(StorageCore.getMediaLocation(), folder);
|
||||
}
|
||||
|
||||
static getPersonThumbnailPath(person: ThumbnailPathEntity) {
|
||||
@@ -108,7 +123,7 @@ export class StorageCore {
|
||||
|
||||
static isImmichPath(path: string) {
|
||||
const resolvedPath = resolve(path);
|
||||
const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION);
|
||||
const resolvedAppMediaLocation = StorageCore.getMediaLocation();
|
||||
const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/';
|
||||
const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/')
|
||||
? resolvedAppMediaLocation
|
||||
|
||||
@@ -192,6 +192,7 @@ export type SharedLink = {
|
||||
showExif: boolean;
|
||||
type: SharedLinkType;
|
||||
userId: string;
|
||||
slug: string | null;
|
||||
};
|
||||
|
||||
export type Album = Selectable<AlbumTable> & {
|
||||
@@ -201,7 +202,6 @@ export type Album = Selectable<AlbumTable> & {
|
||||
|
||||
export type AuthSession = {
|
||||
id: string;
|
||||
isPendingSyncReset: boolean;
|
||||
hasElevatedPermission: boolean;
|
||||
};
|
||||
|
||||
@@ -308,7 +308,7 @@ export const columns = {
|
||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
||||
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
||||
authApiKey: ['api_key.id', 'api_key.permissions'],
|
||||
authSession: ['session.id', 'session.isPendingSyncReset', 'session.updatedAt', 'session.pinExpiresAt'],
|
||||
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt'],
|
||||
authSharedLink: [
|
||||
'shared_link.id',
|
||||
'shared_link.userId',
|
||||
@@ -353,9 +353,11 @@ export const columns = {
|
||||
'asset.duration',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.stackId',
|
||||
'asset.libraryId',
|
||||
],
|
||||
syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'],
|
||||
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
||||
syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId', 'profileImagePath', 'profileChangedAt'],
|
||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||
syncAssetExif: [
|
||||
'asset_exif.assetId',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
@@ -87,7 +87,7 @@ export function Chunked(
|
||||
|
||||
return Promise.all(
|
||||
chunks(argument, chunkSize).map(async (chunk) => {
|
||||
await Reflect.apply(originalMethod, this, [
|
||||
return await Reflect.apply(originalMethod, this, [
|
||||
...arguments_.slice(0, parameterIndex),
|
||||
chunk,
|
||||
...arguments_.slice(parameterIndex + 1),
|
||||
@@ -103,7 +103,7 @@ export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator
|
||||
}
|
||||
|
||||
export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
|
||||
return Chunked({ ...options, mergeFn: setUnion });
|
||||
return Chunked({ ...options, mergeFn: (args: Set<any>[]) => setUnion(...args) });
|
||||
}
|
||||
|
||||
const UUID = '00000000-0000-4000-a000-000000000000';
|
||||
@@ -159,12 +159,21 @@ type LifecycleMetadata = {
|
||||
deprecatedAt?: LifecycleRelease;
|
||||
};
|
||||
|
||||
export const EndpointLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => {
|
||||
export const EndpointLifecycle = ({
|
||||
addedAt,
|
||||
deprecatedAt,
|
||||
description,
|
||||
...options
|
||||
}: LifecycleMetadata & ApiOperationOptions) => {
|
||||
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
|
||||
if (deprecatedAt) {
|
||||
decorators.push(
|
||||
ApiTags('Deprecated'),
|
||||
ApiOperation({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }),
|
||||
ApiOperation({
|
||||
deprecated: true,
|
||||
description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''),
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
@@ -54,6 +55,20 @@ export class CreateAlbumDto {
|
||||
assetIds?: string[];
|
||||
}
|
||||
|
||||
export class AlbumsAddAssetsDto {
|
||||
@ValidateUUID({ each: true })
|
||||
albumIds!: string[];
|
||||
|
||||
@ValidateUUID({ each: true })
|
||||
assetIds!: string[];
|
||||
}
|
||||
|
||||
export class AlbumsAddAssetsResponseDto {
|
||||
success!: boolean;
|
||||
@ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true })
|
||||
error?: BulkIdErrorReason;
|
||||
}
|
||||
|
||||
export class UpdateAlbumDto {
|
||||
@Optional()
|
||||
@IsString()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { plainToInstance, Transform, Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
@@ -64,6 +66,20 @@ export class AssetMediaCreateDto extends AssetMediaBase {
|
||||
@ValidateUUID({ optional: true })
|
||||
livePhotoVideoId?: string;
|
||||
|
||||
@Transform(({ value }) => {
|
||||
try {
|
||||
const json = JSON.parse(value);
|
||||
const items = Array.isArray(json) ? json : [json];
|
||||
return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item));
|
||||
} catch {
|
||||
throw new BadRequestException(['metadata must be valid JSON']);
|
||||
}
|
||||
})
|
||||
@Optional()
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
metadata!: AssetMetadataUpsertItemDto[];
|
||||
|
||||
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
||||
[UploadFieldName.SIDECAR_DATA]?: any;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ export class SanitizedAssetResponseDto {
|
||||
}
|
||||
|
||||
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'The UTC timestamp when the asset was originally uploaded to Immich.',
|
||||
example: '2024-01-15T20:30:00.000Z',
|
||||
})
|
||||
createdAt!: Date;
|
||||
deviceAssetId!: string;
|
||||
deviceId!: string;
|
||||
ownerId!: string;
|
||||
@@ -190,6 +197,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt,
|
||||
deviceAssetId: entity.deviceAssetId,
|
||||
ownerId: entity.ownerId,
|
||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsLatitude,
|
||||
IsLongitude,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsPositive,
|
||||
IsString,
|
||||
IsTimeZone,
|
||||
Max,
|
||||
Min,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetMetadataKey, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
import { AssetMetadata, AssetMetadataItem } from 'src/types';
|
||||
import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class DeviceIdDto {
|
||||
@IsNotEmpty()
|
||||
@@ -65,6 +70,16 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||
|
||||
@Optional()
|
||||
duplicateId?: string | null;
|
||||
|
||||
@IsNotSiblingOf(['dateTimeOriginal'])
|
||||
@Optional()
|
||||
@IsInt()
|
||||
dateTimeRelative?: number;
|
||||
|
||||
@IsNotSiblingOf(['dateTimeOriginal'])
|
||||
@IsTimeZone()
|
||||
@Optional()
|
||||
timeZone?: string;
|
||||
}
|
||||
|
||||
export class UpdateAssetDto extends UpdateAssetBase {
|
||||
@@ -124,6 +139,53 @@ export class AssetStatsResponseDto {
|
||||
total!: number;
|
||||
}
|
||||
|
||||
export class AssetMetadataRouteParams {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
|
||||
@ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' })
|
||||
key!: AssetMetadataKey;
|
||||
}
|
||||
|
||||
export class AssetMetadataUpsertDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetMetadataUpsertItemDto)
|
||||
items!: AssetMetadataUpsertItemDto[];
|
||||
}
|
||||
|
||||
export class AssetMetadataUpsertItemDto implements AssetMetadataItem {
|
||||
@ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' })
|
||||
key!: AssetMetadataKey;
|
||||
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type((options) => {
|
||||
switch (options?.object.key) {
|
||||
case AssetMetadataKey.MobileApp: {
|
||||
return AssetMetadataMobileAppDto;
|
||||
}
|
||||
default: {
|
||||
return Object;
|
||||
}
|
||||
}
|
||||
})
|
||||
value!: AssetMetadata[AssetMetadataKey];
|
||||
}
|
||||
|
||||
export class AssetMetadataMobileAppDto {
|
||||
@IsString()
|
||||
@Optional()
|
||||
iCloudId?: string;
|
||||
}
|
||||
|
||||
export class AssetMetadataResponseDto {
|
||||
@ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' })
|
||||
key!: AssetMetadataKey;
|
||||
value!: object;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
|
||||
return {
|
||||
images: stats[AssetType.Image],
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { PartnerDirection } from 'src/repositories/partner.repository';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
import { ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
export class PartnerCreateDto {
|
||||
@ValidateUUID()
|
||||
sharedWithId!: string;
|
||||
}
|
||||
|
||||
export class PartnerUpdateDto {
|
||||
@IsNotEmpty()
|
||||
inTimeline!: boolean;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||
|
||||
class BaseSearchDto {
|
||||
@ValidateUUID({ optional: true, nullable: true })
|
||||
@@ -126,6 +126,15 @@ export class RandomSearchDto extends BaseSearchWithResultsDto {
|
||||
withPeople?: boolean;
|
||||
}
|
||||
|
||||
export class LargeAssetSearchDto extends BaseSearchWithResultsDto {
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
minFileSize?: number;
|
||||
}
|
||||
|
||||
export class MetadataSearchDto extends RandomSearchDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
@@ -135,9 +144,7 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
@Optional()
|
||||
deviceAssetId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@ValidateString({ optional: true, trim: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@@ -145,9 +152,7 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
@Optional()
|
||||
checksum?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@ValidateString({ optional: true, trim: true })
|
||||
originalFileName?: string;
|
||||
|
||||
@IsString()
|
||||
@@ -181,16 +186,17 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
}
|
||||
|
||||
export class StatisticsSearchDto extends BaseSearchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@ValidateString({ optional: true, trim: true })
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
query!: string;
|
||||
@ValidateString({ optional: true, trim: true })
|
||||
query?: string;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
@Optional()
|
||||
queryAssetId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Session } from 'src/database';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
@@ -22,7 +22,8 @@ export class SessionCreateDto {
|
||||
|
||||
export class SessionUpdateDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isPendingSyncReset?: boolean;
|
||||
@Equals(true)
|
||||
isPendingSyncReset?: true;
|
||||
}
|
||||
|
||||
export class SessionResponseDto {
|
||||
|
||||
@@ -22,13 +22,17 @@ export class SharedLinkCreateDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
albumId?: string;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
@Optional()
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
@Optional()
|
||||
password?: string;
|
||||
password?: string | null;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
slug?: string | null;
|
||||
|
||||
@ValidateDate({ optional: true, nullable: true })
|
||||
expiresAt?: Date | null = null;
|
||||
@@ -44,16 +48,22 @@ export class SharedLinkCreateDto {
|
||||
}
|
||||
|
||||
export class SharedLinkEditDto {
|
||||
@Optional()
|
||||
description?: string;
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
|
||||
@Optional()
|
||||
password?: string;
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
password?: string | null;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsString()
|
||||
slug?: string | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
expiresAt?: Date | null;
|
||||
|
||||
@Optional()
|
||||
@ValidateBoolean({ optional: true })
|
||||
allowUpload?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -99,6 +109,8 @@ export class SharedLinkResponseDto {
|
||||
|
||||
allowDownload!: boolean;
|
||||
showMetadata!: boolean;
|
||||
|
||||
slug!: string | null;
|
||||
}
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||
@@ -118,6 +130,7 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
slug: sharedLink.slug,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,5 +154,6 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLink
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
slug: sharedLink.slug,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetMetadataKey,
|
||||
AssetOrder,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
SyncEntityType,
|
||||
SyncRequestType,
|
||||
UserAvatarColor,
|
||||
UserMetadataKey,
|
||||
} from 'src/enum';
|
||||
import { UserMetadata } from 'src/types';
|
||||
@@ -58,7 +60,23 @@ export class SyncUserV1 {
|
||||
id!: string;
|
||||
name!: string;
|
||||
email!: string;
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true })
|
||||
avatarColor!: UserAvatarColor | null;
|
||||
deletedAt!: Date | null;
|
||||
hasProfileImage!: boolean;
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAuthUserV1 extends SyncUserV1 {
|
||||
isAdmin!: boolean;
|
||||
pinCode!: string | null;
|
||||
oauthId!: string;
|
||||
storageLabel!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
quotaSizeInBytes!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
quotaUsageInBytes!: number;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
@@ -98,6 +116,7 @@ export class SyncAssetV1 {
|
||||
visibility!: AssetVisibility;
|
||||
livePhotoVideoId!: string | null;
|
||||
stackId!: string | null;
|
||||
libraryId!: string | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
@@ -144,6 +163,21 @@ export class SyncAssetExifV1 {
|
||||
fps!: number | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetMetadataV1 {
|
||||
assetId!: string;
|
||||
@ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' })
|
||||
key!: AssetMetadataKey;
|
||||
value!: object;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetMetadataDeleteV1 {
|
||||
assetId!: string;
|
||||
@ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' })
|
||||
key!: AssetMetadataKey;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumDeleteV1 {
|
||||
albumId!: string;
|
||||
@@ -261,11 +295,17 @@ export class SyncAssetFaceV1 {
|
||||
id!: string;
|
||||
assetId!: string;
|
||||
personId!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageWidth!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageHeight!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX2!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY2!: number;
|
||||
sourceType!: string;
|
||||
}
|
||||
@@ -278,14 +318,16 @@ export class SyncAssetFaceDeleteV1 {
|
||||
@ExtraModel()
|
||||
export class SyncUserMetadataV1 {
|
||||
userId!: string;
|
||||
key!: string;
|
||||
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' })
|
||||
key!: UserMetadataKey;
|
||||
value!: UserMetadata[UserMetadataKey];
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncUserMetadataDeleteV1 {
|
||||
userId!: string;
|
||||
key!: string;
|
||||
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' })
|
||||
key!: UserMetadataKey;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
@@ -294,13 +336,19 @@ export class SyncAckV1 {}
|
||||
@ExtraModel()
|
||||
export class SyncResetV1 {}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncCompleteV1 {}
|
||||
|
||||
export type SyncItem = {
|
||||
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
||||
[SyncEntityType.UserV1]: SyncUserV1;
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
|
||||
[SyncEntityType.AssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1;
|
||||
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
|
||||
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
|
||||
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1;
|
||||
@@ -312,9 +360,11 @@ export type SyncItem = {
|
||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||
[SyncEntityType.AlbumAssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetCreateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetUpdateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifCreateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifUpdateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1;
|
||||
[SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1;
|
||||
@@ -335,6 +385,7 @@ export type SyncItem = {
|
||||
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
||||
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||
[SyncEntityType.SyncCompleteV1]: SyncCompleteV1;
|
||||
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Exclude, Transform, Type } from 'class-transformer';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsInt,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
@@ -257,21 +256,32 @@ class SystemConfigLoggingDto {
|
||||
level!: LogLevel;
|
||||
}
|
||||
|
||||
class MachineLearningAvailabilityChecksDto {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@IsInt()
|
||||
timeout!: number;
|
||||
|
||||
@IsInt()
|
||||
interval!: number;
|
||||
}
|
||||
|
||||
class SystemConfigMachineLearningDto {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.122.0' })
|
||||
@Exclude()
|
||||
url?: string;
|
||||
|
||||
@IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
|
||||
@ArrayMinSize(1)
|
||||
@Transform(({ obj, value }) => (obj.url ? [obj.url] : value))
|
||||
@ValidateIf((dto) => dto.enabled)
|
||||
@ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 })
|
||||
urls!: string[];
|
||||
|
||||
@Type(() => MachineLearningAvailabilityChecksDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
availabilityChecks!: MachineLearningAvailabilityChecksDto;
|
||||
|
||||
@Type(() => CLIPConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
||||
@@ -53,6 +53,12 @@ export class TimeBucketDto {
|
||||
description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)',
|
||||
})
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@ValidateBoolean({
|
||||
optional: true,
|
||||
description: 'Include location data in the response',
|
||||
})
|
||||
withCoordinates?: boolean;
|
||||
}
|
||||
|
||||
export class TimeBucketAssetDto extends TimeBucketDto {
|
||||
@@ -185,6 +191,22 @@ export class TimeBucketAssetResponseDto {
|
||||
description: 'Array of country names extracted from EXIF GPS data',
|
||||
})
|
||||
country!: (string | null)[];
|
||||
|
||||
@ApiProperty({
|
||||
type: 'array',
|
||||
required: false,
|
||||
items: { type: 'number', nullable: true },
|
||||
description: 'Array of latitude coordinates extracted from EXIF GPS data',
|
||||
})
|
||||
latitude!: number[];
|
||||
|
||||
@ApiProperty({
|
||||
type: 'array',
|
||||
required: false,
|
||||
items: { type: 'number', nullable: true },
|
||||
description: 'Array of longitude coordinates extracted from EXIF GPS data',
|
||||
})
|
||||
longitude!: number[];
|
||||
}
|
||||
|
||||
export class TimeBucketsResponseDto {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
|
||||
import { User, UserAdmin } from 'src/database';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
@@ -91,7 +91,7 @@ export class UserAdminCreateDto {
|
||||
storageLabel?: string | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsNumber()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes?: number | null;
|
||||
@@ -137,7 +137,7 @@ export class UserAdminUpdateDto {
|
||||
shouldChangePassword?: boolean;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsNumber()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes?: number | null;
|
||||
|
||||
@@ -29,8 +29,8 @@ export const AlbumUpdateEmail = ({
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
New media has been added to <strong>{albumName}</strong>,
|
||||
<br /> check it out!
|
||||
New media has been added to <strong>{albumName}</strong>.
|
||||
<br /> Check it out!
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, ButtonProps } from '@react-email/components';
|
||||
import { Button, ButtonProps, Text } from '@react-email/components';
|
||||
|
||||
export const ImmichButton = ({ children, ...props }: ButtonProps) => (
|
||||
<Button
|
||||
{...props}
|
||||
className="py-3 px-8 border bg-immich-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 font-bold uppercase"
|
||||
className="border bg-immich-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 font-bold uppercase"
|
||||
>
|
||||
{children}
|
||||
<Text className="my-3 mx-8">{children}</Text>
|
||||
</Button>
|
||||
);
|
||||
|
||||
+92
-11
@@ -17,12 +17,14 @@ export enum ImmichHeader {
|
||||
UserToken = 'x-immich-user-token',
|
||||
SessionToken = 'x-immich-session-token',
|
||||
SharedLinkKey = 'x-immich-share-key',
|
||||
SharedLinkSlug = 'x-immich-share-slug',
|
||||
Checksum = 'x-immich-checksum',
|
||||
Cid = 'x-immich-cid',
|
||||
}
|
||||
|
||||
export enum ImmichQuery {
|
||||
SharedLinkKey = 'key',
|
||||
SharedLinkSlug = 'slug',
|
||||
ApiKey = 'apiKey',
|
||||
SessionKey = 'sessionKey',
|
||||
}
|
||||
@@ -87,31 +89,45 @@ export enum Permission {
|
||||
AssetRead = 'asset.read',
|
||||
AssetUpdate = 'asset.update',
|
||||
AssetDelete = 'asset.delete',
|
||||
AssetStatistics = 'asset.statistics',
|
||||
AssetShare = 'asset.share',
|
||||
AssetView = 'asset.view',
|
||||
AssetDownload = 'asset.download',
|
||||
AssetUpload = 'asset.upload',
|
||||
AssetReplace = 'asset.replace',
|
||||
|
||||
AlbumCreate = 'album.create',
|
||||
AlbumRead = 'album.read',
|
||||
AlbumUpdate = 'album.update',
|
||||
AlbumDelete = 'album.delete',
|
||||
AlbumStatistics = 'album.statistics',
|
||||
|
||||
AlbumAddAsset = 'album.addAsset',
|
||||
AlbumRemoveAsset = 'album.removeAsset',
|
||||
AlbumShare = 'album.share',
|
||||
AlbumDownload = 'album.download',
|
||||
|
||||
AlbumAssetCreate = 'albumAsset.create',
|
||||
AlbumAssetDelete = 'albumAsset.delete',
|
||||
|
||||
AlbumUserCreate = 'albumUser.create',
|
||||
AlbumUserUpdate = 'albumUser.update',
|
||||
AlbumUserDelete = 'albumUser.delete',
|
||||
|
||||
AuthChangePassword = 'auth.changePassword',
|
||||
|
||||
AuthDeviceDelete = 'authDevice.delete',
|
||||
|
||||
ArchiveRead = 'archive.read',
|
||||
|
||||
DuplicateRead = 'duplicate.read',
|
||||
DuplicateDelete = 'duplicate.delete',
|
||||
|
||||
FaceCreate = 'face.create',
|
||||
FaceRead = 'face.read',
|
||||
FaceUpdate = 'face.update',
|
||||
FaceDelete = 'face.delete',
|
||||
|
||||
JobCreate = 'job.create',
|
||||
JobRead = 'job.read',
|
||||
|
||||
LibraryCreate = 'library.create',
|
||||
LibraryRead = 'library.read',
|
||||
LibraryUpdate = 'library.update',
|
||||
@@ -125,6 +141,10 @@ export enum Permission {
|
||||
MemoryRead = 'memory.read',
|
||||
MemoryUpdate = 'memory.update',
|
||||
MemoryDelete = 'memory.delete',
|
||||
MemoryStatistics = 'memory.statistics',
|
||||
|
||||
MemoryAssetCreate = 'memoryAsset.create',
|
||||
MemoryAssetDelete = 'memoryAsset.delete',
|
||||
|
||||
NotificationCreate = 'notification.create',
|
||||
NotificationRead = 'notification.read',
|
||||
@@ -144,6 +164,20 @@ export enum Permission {
|
||||
PersonMerge = 'person.merge',
|
||||
PersonReassign = 'person.reassign',
|
||||
|
||||
PinCodeCreate = 'pinCode.create',
|
||||
PinCodeUpdate = 'pinCode.update',
|
||||
PinCodeDelete = 'pinCode.delete',
|
||||
|
||||
ServerAbout = 'server.about',
|
||||
ServerApkLinks = 'server.apkLinks',
|
||||
ServerStorage = 'server.storage',
|
||||
ServerStatistics = 'server.statistics',
|
||||
ServerVersionCheck = 'server.versionCheck',
|
||||
|
||||
ServerLicenseRead = 'serverLicense.read',
|
||||
ServerLicenseUpdate = 'serverLicense.update',
|
||||
ServerLicenseDelete = 'serverLicense.delete',
|
||||
|
||||
SessionCreate = 'session.create',
|
||||
SessionRead = 'session.read',
|
||||
SessionUpdate = 'session.update',
|
||||
@@ -160,6 +194,11 @@ export enum Permission {
|
||||
StackUpdate = 'stack.update',
|
||||
StackDelete = 'stack.delete',
|
||||
|
||||
SyncStream = 'sync.stream',
|
||||
SyncCheckpointRead = 'syncCheckpoint.read',
|
||||
SyncCheckpointUpdate = 'syncCheckpoint.update',
|
||||
SyncCheckpointDelete = 'syncCheckpoint.delete',
|
||||
|
||||
SystemConfigRead = 'systemConfig.read',
|
||||
SystemConfigUpdate = 'systemConfig.update',
|
||||
|
||||
@@ -172,10 +211,32 @@ export enum Permission {
|
||||
TagDelete = 'tag.delete',
|
||||
TagAsset = 'tag.asset',
|
||||
|
||||
AdminUserCreate = 'admin.user.create',
|
||||
AdminUserRead = 'admin.user.read',
|
||||
AdminUserUpdate = 'admin.user.update',
|
||||
AdminUserDelete = 'admin.user.delete',
|
||||
UserRead = 'user.read',
|
||||
UserUpdate = 'user.update',
|
||||
|
||||
UserLicenseCreate = 'userLicense.create',
|
||||
UserLicenseRead = 'userLicense.read',
|
||||
UserLicenseUpdate = 'userLicense.update',
|
||||
UserLicenseDelete = 'userLicense.delete',
|
||||
|
||||
UserOnboardingRead = 'userOnboarding.read',
|
||||
UserOnboardingUpdate = 'userOnboarding.update',
|
||||
UserOnboardingDelete = 'userOnboarding.delete',
|
||||
|
||||
UserPreferenceRead = 'userPreference.read',
|
||||
UserPreferenceUpdate = 'userPreference.update',
|
||||
|
||||
UserProfileImageCreate = 'userProfileImage.create',
|
||||
UserProfileImageRead = 'userProfileImage.read',
|
||||
UserProfileImageUpdate = 'userProfileImage.update',
|
||||
UserProfileImageDelete = 'userProfileImage.delete',
|
||||
|
||||
AdminUserCreate = 'adminUser.create',
|
||||
AdminUserRead = 'adminUser.read',
|
||||
AdminUserUpdate = 'adminUser.update',
|
||||
AdminUserDelete = 'adminUser.delete',
|
||||
|
||||
AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
|
||||
}
|
||||
|
||||
export enum SharedLinkType {
|
||||
@@ -215,6 +276,10 @@ export enum UserMetadataKey {
|
||||
Onboarding = 'onboarding',
|
||||
}
|
||||
|
||||
export enum AssetMetadataKey {
|
||||
MobileApp = 'mobile-app',
|
||||
}
|
||||
|
||||
export enum UserAvatarColor {
|
||||
Primary = 'primary',
|
||||
Pink = 'pink',
|
||||
@@ -355,6 +420,11 @@ export enum LogLevel {
|
||||
Fatal = 'fatal',
|
||||
}
|
||||
|
||||
export enum ApiCustomExtension {
|
||||
Permission = 'x-immich-permission',
|
||||
AdminOnly = 'x-immich-admin-only',
|
||||
}
|
||||
|
||||
export enum MetadataKey {
|
||||
AuthRoute = 'auth_route',
|
||||
AdminRoute = 'admin_route',
|
||||
@@ -417,6 +487,8 @@ export enum DatabaseExtension {
|
||||
export enum BootstrapEventPriority {
|
||||
// Database service should be initialized before anything else, most other services need database access
|
||||
DatabaseService = -200,
|
||||
// Detect and configure the media location before jobs are queued which may use it
|
||||
StorageService = -195,
|
||||
// Other services may need to queue jobs on bootstrap.
|
||||
JobService = -190,
|
||||
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||
@@ -458,6 +530,7 @@ export enum JobName {
|
||||
AssetGenerateThumbnails = 'AssetGenerateThumbnails',
|
||||
|
||||
AuditLogCleanup = 'AuditLogCleanup',
|
||||
AuditTableCleanup = 'AuditTableCleanup',
|
||||
|
||||
DatabaseBackup = 'DatabaseBackup',
|
||||
|
||||
@@ -498,8 +571,7 @@ export enum JobName {
|
||||
SendMail = 'SendMail',
|
||||
|
||||
SidecarQueueAll = 'SidecarQueueAll',
|
||||
SidecarDiscovery = 'SidecarDiscovery',
|
||||
SidecarSync = 'SidecarSync',
|
||||
SidecarCheck = 'SidecarCheck',
|
||||
SidecarWrite = 'SidecarWrite',
|
||||
|
||||
SmartSearchQueueAll = 'SmartSearchQueueAll',
|
||||
@@ -559,6 +631,8 @@ export enum SyncRequestType {
|
||||
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||
AssetsV1 = 'AssetsV1',
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AuthUsersV1 = 'AuthUsersV1',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
PartnersV1 = 'PartnersV1',
|
||||
@@ -573,12 +647,16 @@ export enum SyncRequestType {
|
||||
}
|
||||
|
||||
export enum SyncEntityType {
|
||||
AuthUserV1 = 'AuthUserV1',
|
||||
|
||||
UserV1 = 'UserV1',
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetDeleteV1 = 'AssetDeleteV1',
|
||||
AssetExifV1 = 'AssetExifV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1',
|
||||
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
@@ -599,9 +677,11 @@ export enum SyncEntityType {
|
||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||
|
||||
AlbumAssetV1 = 'AlbumAssetV1',
|
||||
AlbumAssetCreateV1 = 'AlbumAssetCreateV1',
|
||||
AlbumAssetUpdateV1 = 'AlbumAssetUpdateV1',
|
||||
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||
AlbumAssetExifV1 = 'AlbumAssetExifV1',
|
||||
AlbumAssetExifCreateV1 = 'AlbumAssetExifCreateV1',
|
||||
AlbumAssetExifUpdateV1 = 'AlbumAssetExifUpdateV1',
|
||||
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||
|
||||
AlbumToAssetV1 = 'AlbumToAssetV1',
|
||||
@@ -628,6 +708,7 @@ export enum SyncEntityType {
|
||||
|
||||
SyncAckV1 = 'SyncAckV1',
|
||||
SyncResetV1 = 'SyncResetV1',
|
||||
SyncCompleteV1 = 'SyncCompleteV1',
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
|
||||
@@ -7,28 +7,39 @@ import {
|
||||
createParamDecorator,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiCookieAuth, ApiExtension, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ImmichQuery, MetadataKey, Permission } from 'src/enum';
|
||||
import { ApiCustomExtension, ImmichQuery, MetadataKey, Permission } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
type AdminRoute = { admin?: true };
|
||||
type SharedLinkRoute = { sharedLink?: true };
|
||||
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
||||
type AuthenticatedOptions = { permission?: Permission | false } & (AdminRoute | SharedLinkRoute);
|
||||
|
||||
export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => {
|
||||
export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => {
|
||||
const decorators: MethodDecorator[] = [
|
||||
ApiBearerAuth(),
|
||||
ApiCookieAuth(),
|
||||
ApiSecurity(MetadataKey.ApiKeySecurity),
|
||||
SetMetadata(MetadataKey.AuthRoute, options || {}),
|
||||
SetMetadata(MetadataKey.AuthRoute, options),
|
||||
];
|
||||
|
||||
if ((options as AdminRoute).admin) {
|
||||
decorators.push(ApiExtension(ApiCustomExtension.AdminOnly, true));
|
||||
}
|
||||
|
||||
if (options?.permission) {
|
||||
decorators.push(ApiExtension(ApiCustomExtension.Permission, options.permission));
|
||||
}
|
||||
|
||||
if ((options as SharedLinkRoute)?.sharedLink) {
|
||||
decorators.push(ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }));
|
||||
decorators.push(
|
||||
ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }),
|
||||
ApiQuery({ name: ImmichQuery.SharedLinkSlug, type: String, required: false }),
|
||||
);
|
||||
}
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { ImmichFile, UploadFile, UploadFiles } from 'src/types';
|
||||
import { asRequest, mapToUploadFile } from 'src/utils/asset.util';
|
||||
import { asUploadRequest, mapToUploadFile } from 'src/utils/asset.util';
|
||||
|
||||
export function getFile(files: UploadFiles, property: 'assetData' | 'sidecarData') {
|
||||
const file = files[property]?.[0];
|
||||
@@ -99,18 +99,21 @@ export class FileUploadInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
|
||||
return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback);
|
||||
return callbackify(() => this.assetService.canUploadFile(asUploadRequest(request, file)), callback);
|
||||
}
|
||||
|
||||
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(
|
||||
() => this.assetService.getUploadFilename(asRequest(request, file)),
|
||||
() => this.assetService.getUploadFilename(asUploadRequest(request, file)),
|
||||
callback as Callback<string>,
|
||||
);
|
||||
}
|
||||
|
||||
private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback<string>);
|
||||
return callbackify(
|
||||
() => this.assetService.getUploadFolder(asUploadRequest(request, file)),
|
||||
callback as Callback<string>,
|
||||
);
|
||||
}
|
||||
|
||||
private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateUserTable1645130759468 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
||||
await queryRunner.query(`
|
||||
create table if not exists users
|
||||
(
|
||||
id uuid default uuid_generate_v4() not null
|
||||
constraint "PK_a3ffb1c0c8416b9fc6f907b7433"
|
||||
primary key,
|
||||
email varchar not null,
|
||||
password varchar not null,
|
||||
salt varchar not null,
|
||||
"createdAt" timestamp default now() not null
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop table users`);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateDeviceInfoTable1645130777674 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
create table if not exists device_info
|
||||
(
|
||||
id serial
|
||||
constraint "PK_b1c15a80b0a4e5f4eebadbdd92c"
|
||||
primary key,
|
||||
"userId" varchar not null,
|
||||
"deviceId" varchar not null,
|
||||
"deviceType" varchar not null,
|
||||
"notificationToken" varchar,
|
||||
"createdAt" timestamp default now() not null,
|
||||
"isAutoBackup" boolean default false not null,
|
||||
constraint "UQ_ebad78f36b10d15fbea8560e107"
|
||||
unique ("userId", "deviceId")
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop table device_info`);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateAssetsTable1645130805273 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
create table if not exists assets
|
||||
(
|
||||
id uuid default uuid_generate_v4() not null
|
||||
constraint "PK_da96729a8b113377cfb6a62439c"
|
||||
primary key,
|
||||
"deviceAssetId" varchar not null,
|
||||
"userId" varchar not null,
|
||||
"deviceId" varchar not null,
|
||||
type varchar not null,
|
||||
"originalPath" varchar not null,
|
||||
"resizePath" varchar,
|
||||
"createdAt" varchar not null,
|
||||
"modifiedAt" varchar not null,
|
||||
"isFavorite" boolean default false not null,
|
||||
"mimeType" varchar,
|
||||
duration varchar,
|
||||
constraint "UQ_b599ab0bd9574958acb0b30a90e"
|
||||
unique ("deviceAssetId", "userId", "deviceId")
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop table assets`);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateExifTable1645130817965 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
create table if not exists exif
|
||||
(
|
||||
id serial
|
||||
constraint "PK_28663352d85078ad0046dafafaa"
|
||||
primary key,
|
||||
"assetId" uuid not null
|
||||
constraint "REL_c0117fdbc50b917ef9067740c4"
|
||||
unique
|
||||
constraint "FK_c0117fdbc50b917ef9067740c44"
|
||||
references assets
|
||||
on delete cascade,
|
||||
make varchar,
|
||||
model varchar,
|
||||
"imageName" varchar,
|
||||
"exifImageWidth" integer,
|
||||
"exifImageHeight" integer,
|
||||
"fileSizeInByte" integer,
|
||||
orientation varchar,
|
||||
"dateTimeOriginal" timestamp with time zone,
|
||||
"modifyDate" timestamp with time zone,
|
||||
"lensModel" varchar,
|
||||
"fNumber" double precision,
|
||||
"focalLength" double precision,
|
||||
iso integer,
|
||||
"exposureTime" double precision,
|
||||
latitude double precision,
|
||||
longitude double precision
|
||||
);
|
||||
|
||||
create unique index if not exists "IDX_c0117fdbc50b917ef9067740c4" on exif ("assetId");
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop table exif`);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateSmartInfoTable1645130870184 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
create table if not exists smart_info
|
||||
(
|
||||
id serial
|
||||
constraint "PK_0beace66440e9713f5c40470e46"
|
||||
primary key,
|
||||
"assetId" uuid not null
|
||||
constraint "UQ_5e3753aadd956110bf3ec0244ac"
|
||||
unique
|
||||
constraint "FK_5e3753aadd956110bf3ec0244ac"
|
||||
references assets
|
||||
on delete cascade,
|
||||
tags text[]
|
||||
);
|
||||
|
||||
create unique index if not exists "IDX_5e3753aadd956110bf3ec0244a"
|
||||
on smart_info ("assetId");
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
drop table smart_info;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddExifTextSearchColumn1646249209023 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '')
|
||||
)
|
||||
) STORED;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateExifTextSearchIndex1646249734844 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX exif_text_searchable_idx
|
||||
ON exif
|
||||
USING GIN (exif_text_searchable_column);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddRegionCityToExIf1646709533213 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN if not exists city varchar;
|
||||
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN if not exists state varchar;
|
||||
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN if not exists country varchar;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN city;
|
||||
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN state;
|
||||
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN country;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddLocationToExifTextSearch1646710459852 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX exif_text_searchable_idx
|
||||
ON exif
|
||||
USING GIN (exif_text_searchable_column);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||
|
||||
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddObjectColumnToSmartInfo1648317474768 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE smart_info
|
||||
ADD COLUMN if not exists objects text[];
|
||||
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE smart_info
|
||||
DROP COLUMN objects;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateSharedAlbumAndRelatedTables1649643216111 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Create shared_albums
|
||||
await queryRunner.query(`
|
||||
create table if not exists shared_albums
|
||||
(
|
||||
id uuid default uuid_generate_v4() not null
|
||||
constraint "PK_7f71c7b5bc7c87b8f94c9a93a00"
|
||||
primary key,
|
||||
"ownerId" varchar not null,
|
||||
"albumName" varchar default 'Untitled Album'::character varying not null,
|
||||
"createdAt" timestamp with time zone default now() not null,
|
||||
"albumThumbnailAssetId" varchar
|
||||
);
|
||||
|
||||
comment on column shared_albums."albumThumbnailAssetId" is 'Asset ID to be used as thumbnail';
|
||||
`);
|
||||
|
||||
// Create user_shared_album
|
||||
await queryRunner.query(`
|
||||
create table if not exists user_shared_album
|
||||
(
|
||||
id serial
|
||||
constraint "PK_b6562316a98845a7b3e9a25cdd0"
|
||||
primary key,
|
||||
"albumId" uuid not null
|
||||
constraint "FK_7b3bf0f5f8da59af30519c25f18"
|
||||
references shared_albums
|
||||
on delete cascade,
|
||||
"sharedUserId" uuid not null
|
||||
constraint "FK_543c31211653e63e080ba882eb5"
|
||||
references users,
|
||||
constraint "PK_unique_user_in_album"
|
||||
unique ("albumId", "sharedUserId")
|
||||
);
|
||||
`);
|
||||
|
||||
// Create asset_shared_album
|
||||
await queryRunner.query(
|
||||
`
|
||||
create table if not exists asset_shared_album
|
||||
(
|
||||
id serial
|
||||
constraint "PK_a34e076afbc601d81938e2c2277"
|
||||
primary key,
|
||||
"albumId" uuid not null
|
||||
constraint "FK_a8b79a84996cef6ba6a3662825d"
|
||||
references shared_albums
|
||||
on delete cascade,
|
||||
"assetId" uuid not null
|
||||
constraint "FK_64f2e7d68d1d1d8417acc844a4a"
|
||||
references assets
|
||||
on delete cascade,
|
||||
constraint "UQ_a1e2734a1ce361e7a26f6b28288"
|
||||
unique ("albumId", "assetId")
|
||||
);
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
drop table asset_shared_album;
|
||||
drop table user_shared_album;
|
||||
drop table shared_albums;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table users
|
||||
add column if not exists "firstName" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "lastName" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "profileImagePath" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "isAdmin" bool default false;
|
||||
|
||||
alter table users
|
||||
add column if not exists "isFirstLoggedIn" bool default true;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table users
|
||||
drop column "firstName";
|
||||
|
||||
alter table users
|
||||
drop column "lastName";
|
||||
|
||||
alter table users
|
||||
drop column "isAdmin";
|
||||
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table assets
|
||||
add column if not exists "webpPath" varchar default '';
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table assets
|
||||
drop column if exists "webpPath";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateAssetTableWithEncodeVideoPath1654299904583 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table assets
|
||||
add column if not exists "encodedVideoPath" varchar default '';
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table assets
|
||||
drop column if exists "encodedVideoPath";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameSharedAlbums1655401127251 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE shared_albums RENAME TO albums;
|
||||
|
||||
ALTER TABLE asset_shared_album RENAME TO asset_album;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_album RENAME TO asset_shared_album;
|
||||
|
||||
ALTER TABLE albums RENAME TO shared_albums;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameIsFirstLoggedInColumn1656338626260 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE users
|
||||
RENAME COLUMN "isFirstLoggedIn" to "shouldChangePassword";
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE users
|
||||
RENAME COLUMN "shouldChangePassword" to "isFirstLoggedIn";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
|
||||
await queryRunner.query(
|
||||
`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
|
||||
await queryRunner.query(
|
||||
`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||
|
||||
ALTER TABLE exif
|
||||
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX exif_text_searchable_idx
|
||||
ON exif
|
||||
USING GIN (exif_text_searchable_column);
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddExifImageNameAsSearchableText1658860470248 implements MigrationInterface {
|
||||
name = 'AddExifImageNameAsSearchableText1658860470248';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("imageName", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector NOT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddCaption1661011331242 implements MigrationInterface {
|
||||
name = 'AddCaption1661011331242';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "description" text DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "fps" double precision`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "fps"`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "description"`);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ChangeExifFileSizeInByteToBigInt1661528919411 implements MigrationInterface {
|
||||
name = 'ChangeExifFileSizeInByteToBigInt1661528919411';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ALTER COLUMN "fileSizeInByte" type bigint using "fileSizeInByte"::bigint;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
ALTER COLUMN "fileSizeInByte" type integer using "fileSizeInByte"::integer;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddAssetChecksum1661881837496 implements MigrationInterface {
|
||||
name = 'AddAssetChecksum1661881837496';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
|
||||
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_userid_checksum" UNIQUE ("userId", "checksum")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class FixTimestampDataTypeInAssetTable1662427365521 implements MigrationInterface {
|
||||
name = 'FixTimestampDataTypeInAssetTable1662427365521';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "assets" ALTER COLUMN "createdAt" TYPE timestamptz USING "createdAt"::timestamptz`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "assets" ALTER COLUMN "modifiedAt" TYPE timestamptz USING "createdAt"::timestamptz`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "createdAt" TYPE varchar USING "createdAt"::varchar`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "modifiedAt" TYPE varchar USING "createdAt"::varchar`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" DROP NOT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateSystemConfigTable1665540663419 implements MigrationInterface {
|
||||
name = 'CreateSystemConfigTable1665540663419';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" character varying, CONSTRAINT "PK_aab69295b445016f56731f4d535" PRIMARY KEY ("key"))`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "system_config"`);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddingDeletedAtColumnInUserEntity1667762360744 implements MigrationInterface {
|
||||
name = 'AddingDeletedAtColumnInUserEntity1667762360744';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "deletedAt" TIMESTAMP`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deletedAt"`);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddLivePhotosRelatedColumnToAssetTable1668383120461 implements MigrationInterface {
|
||||
name = 'AddLivePhotosRelatedColumnToAssetTable1668383120461'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "isVisible" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "livePhotoVideoId" uuid`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "livePhotoVideoId"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isVisible"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class UpdateUserTableForOIDC1668835311083 implements MigrationInterface {
|
||||
name = 'UpdateUserTableForOIDC1668835311083'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "password" SET DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "salt" SET DEFAULT ''`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "salt" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "password" DROP DEFAULT`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class OAuthId1670104716264 implements MigrationInterface {
|
||||
name = 'OAuthId1670104716264'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "oauthId" character varying NOT NULL DEFAULT ''`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "oauthId"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class CreateTagsTable1670257571385 implements MigrationInterface {
|
||||
name = 'CreateTagsTable1670257571385'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "name" character varying NOT NULL, "userId" uuid NOT NULL, "renameTagId" uuid, CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId"), CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id")); COMMENT ON COLUMN "tags"."renameTagId" IS 'The new renamed tagId'`);
|
||||
await queryRunner.query(`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL, CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId") `);
|
||||
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`);
|
||||
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`);
|
||||
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_f8e8a9e893cb5c54907f1b798e"`);
|
||||
await queryRunner.query(`DROP TABLE "tag_asset"`);
|
||||
await queryRunner.query(`DROP TABLE "tags"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class TruncateOldConfigItems1670607437008 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`TRUNCATE TABLE "system_config"`);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddUserEmailUniqueConstraint1670633210032 implements MigrationInterface {
|
||||
name = 'AddUserEmailUniqueConstraint1670633210032'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DropSaltColumn1672109862870 implements MigrationInterface {
|
||||
name = 'DropSaltColumn1672109862870'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "salt"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "salt" character varying NOT NULL DEFAULT ''`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddAPIKeys1672502270115 implements MigrationInterface {
|
||||
name = 'AddAPIKeys1672502270115'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "api_keys" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
|
||||
await queryRunner.query(`DROP TABLE "api_keys"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddSharedLinkTable1673150490490 implements MigrationInterface {
|
||||
name = 'AddSharedLinkTable1673150490490'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" character varying NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key"), CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_sharedlink_key" ON "shared_links" ("key") `);
|
||||
await queryRunner.query(`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL, CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId") `);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_c9fab4aa97ffd1b034f3d6581a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_5b7decce6c8d3db9593d6111a6"`);
|
||||
await queryRunner.query(`DROP TABLE "shared_link__asset"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_sharedlink_key"`);
|
||||
await queryRunner.query(`DROP TABLE "shared_links"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMorePermissionToSharedLink1673907194740 implements MigrationInterface {
|
||||
name = 'AddMorePermissionToSharedLink1673907194740';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD "allowDownload" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD "showExif" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "showExif"`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "allowDownload"`);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user