mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(server,web): server config (#4006)
* feat: server config * chore: open api * fix: redirect /map to /photos when disabled
This commit is contained in:
@@ -3342,6 +3342,27 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/config": {
|
||||
"get": {
|
||||
"operationId": "getServerConfig",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerConfigDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/features": {
|
||||
"get": {
|
||||
"operationId": "getServerFeatures",
|
||||
@@ -6618,6 +6639,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerConfigDto": {
|
||||
"properties": {
|
||||
"loginPageMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapTileUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"oauthButtonText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"oauthButtonText",
|
||||
"loginPageMessage",
|
||||
"mapTileUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerFeaturesDto": {
|
||||
"properties": {
|
||||
"clipEncode": {
|
||||
@@ -6629,6 +6669,9 @@
|
||||
"facialRecognition": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"map": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6649,15 +6692,16 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"configFile",
|
||||
"clipEncode",
|
||||
"configFile",
|
||||
"facialRecognition",
|
||||
"sidecar",
|
||||
"search",
|
||||
"tagImage",
|
||||
"map",
|
||||
"oauth",
|
||||
"oauthAutoLaunch",
|
||||
"passwordLogin"
|
||||
"passwordLogin",
|
||||
"sidecar",
|
||||
"search",
|
||||
"tagImage"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -6989,6 +7033,9 @@
|
||||
"machineLearning": {
|
||||
"$ref": "#/components/schemas/SystemConfigMachineLearningDto"
|
||||
},
|
||||
"map": {
|
||||
"$ref": "#/components/schemas/SystemConfigMapDto"
|
||||
},
|
||||
"oauth": {
|
||||
"$ref": "#/components/schemas/SystemConfigOAuthDto"
|
||||
},
|
||||
@@ -7005,6 +7052,7 @@
|
||||
"required": [
|
||||
"ffmpeg",
|
||||
"machineLearning",
|
||||
"map",
|
||||
"oauth",
|
||||
"passwordLogin",
|
||||
"storageTemplate",
|
||||
@@ -7162,6 +7210,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigMapDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tileUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"tileUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigOAuthDto": {
|
||||
"properties": {
|
||||
"autoLaunch": {
|
||||
|
||||
@@ -79,16 +79,21 @@ export class ServerMediaTypesResponseDto {
|
||||
sidecar!: string[];
|
||||
}
|
||||
|
||||
export class ServerFeaturesDto implements FeatureFlags {
|
||||
configFile!: boolean;
|
||||
clipEncode!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
sidecar!: boolean;
|
||||
search!: boolean;
|
||||
tagImage!: boolean;
|
||||
export class ServerConfigDto {
|
||||
oauthButtonText!: string;
|
||||
loginPageMessage!: string;
|
||||
mapTileUrl!: string;
|
||||
}
|
||||
|
||||
// TODO: use these instead of `POST oauth/config`
|
||||
export class ServerFeaturesDto implements FeatureFlags {
|
||||
clipEncode!: boolean;
|
||||
configFile!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
map!: boolean;
|
||||
oauth!: boolean;
|
||||
oauthAutoLaunch!: boolean;
|
||||
passwordLogin!: boolean;
|
||||
sidecar!: boolean;
|
||||
search!: boolean;
|
||||
tagImage!: boolean;
|
||||
}
|
||||
|
||||
@@ -143,22 +143,34 @@ describe(ServerInfoService.name, () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual(serverVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeatures', () => {
|
||||
it('should respond the server features', async () => {
|
||||
await expect(sut.getFeatures()).resolves.toEqual({
|
||||
clipEncode: true,
|
||||
facialRecognition: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
passwordLogin: true,
|
||||
search: true,
|
||||
sidecar: true,
|
||||
tagImage: true,
|
||||
configFile: false,
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
describe('getFeatures', () => {
|
||||
it('should respond the server features', async () => {
|
||||
await expect(sut.getFeatures()).resolves.toEqual({
|
||||
clipEncode: true,
|
||||
facialRecognition: true,
|
||||
map: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
passwordLogin: true,
|
||||
search: true,
|
||||
sidecar: true,
|
||||
tagImage: true,
|
||||
configFile: false,
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should respond the server configuration', async () => {
|
||||
await expect(sut.getConfig()).resolves.toEqual({
|
||||
loginPageMessage: '',
|
||||
oauthButtonText: 'Login with OAuth',
|
||||
mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||
import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
|
||||
import { IUserRepository, UserStatsQueryResponse } from '../user';
|
||||
import {
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerInfoResponseDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
@@ -55,6 +56,19 @@ export class ServerInfoService {
|
||||
return this.configCore.getFeatures();
|
||||
}
|
||||
|
||||
async getConfig(): Promise<ServerConfigDto> {
|
||||
const config = await this.configCore.getConfig();
|
||||
|
||||
// TODO move to system config
|
||||
const loginPageMessage = process.env.PUBLIC_LOGIN_PAGE_MESSAGE || '';
|
||||
|
||||
return {
|
||||
loginPageMessage,
|
||||
mapTileUrl: config.map.tileUrl,
|
||||
oauthButtonText: config.oauth.buttonText,
|
||||
};
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsBoolean, IsString } from 'class-validator';
|
||||
|
||||
export class SystemConfigMapDto {
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@IsString()
|
||||
tileUrl!: string;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { IsObject, ValidateNested } from 'class-validator';
|
||||
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
|
||||
import { SystemConfigJobDto } from './system-config-job.dto';
|
||||
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
|
||||
import { SystemConfigMapDto } from './system-config-map.dto';
|
||||
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
|
||||
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
|
||||
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
|
||||
@@ -20,6 +21,11 @@ export class SystemConfigDto implements SystemConfig {
|
||||
@IsObject()
|
||||
machineLearning!: SystemConfigMachineLearningDto;
|
||||
|
||||
@Type(() => SystemConfigMapDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
map!: SystemConfigMapDto;
|
||||
|
||||
@Type(() => SystemConfigOAuthDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
||||
@@ -55,7 +55,6 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
},
|
||||
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
|
||||
@@ -75,6 +74,10 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
maxDistance: 0.6,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
},
|
||||
oauth: {
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
@@ -108,6 +111,7 @@ export enum FeatureFlag {
|
||||
CLIP_ENCODE = 'clipEncode',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
TAG_IMAGE = 'tagImage',
|
||||
MAP = 'map',
|
||||
SIDECAR = 'sidecar',
|
||||
SEARCH = 'search',
|
||||
OAUTH = 'oauth',
|
||||
@@ -169,6 +173,7 @@ export class SystemConfigCore {
|
||||
[FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clip.enabled,
|
||||
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
|
||||
[FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled,
|
||||
[FeatureFlag.MAP]: config.map.enabled,
|
||||
[FeatureFlag.SIDECAR]: true,
|
||||
[FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
|
||||
|
||||
|
||||
@@ -73,6 +73,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
maxDistance: 0.6,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
},
|
||||
oauth: {
|
||||
autoLaunch: true,
|
||||
autoRegister: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerInfoResponseDto,
|
||||
ServerInfoService,
|
||||
@@ -42,6 +43,12 @@ export class ServerInfoController {
|
||||
return this.service.getFeatures();
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@Get('config')
|
||||
getServerConfig(): Promise<ServerConfigDto> {
|
||||
return this.service.getConfig();
|
||||
}
|
||||
|
||||
@AdminRoute()
|
||||
@Get('stats')
|
||||
getStats(): Promise<ServerStatsResponseDto> {
|
||||
|
||||
@@ -58,6 +58,9 @@ export enum SystemConfigKey {
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
|
||||
|
||||
MAP_ENABLED = 'map.enabled',
|
||||
MAP_TILE_URL = 'map.tileUrl',
|
||||
|
||||
OAUTH_ENABLED = 'oauth.enabled',
|
||||
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
||||
OAUTH_CLIENT_ID = 'oauth.clientId',
|
||||
@@ -164,6 +167,10 @@ export interface SystemConfig {
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
tileUrl: string;
|
||||
};
|
||||
oauth: {
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
|
||||
@@ -83,6 +83,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
clipEncode: true,
|
||||
configFile: false,
|
||||
facialRecognition: true,
|
||||
map: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
passwordLogin: true,
|
||||
@@ -93,6 +94,18 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /server-info/config', () => {
|
||||
it('should respond with the server configuration', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/config');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
loginPageMessage: '',
|
||||
oauthButtonText: 'Login with OAuth',
|
||||
mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /server-info/stats', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/stats');
|
||||
|
||||
Reference in New Issue
Block a user